tbranyen/diffhtml

Function component: how to use props as slot like custom component?

Dan-Do opened this issue ยท 27 comments

Hi @tbranyen,
Here is my pseudo-code:

function MyComponent(props) {
  return html`
    <div class="modal-dialog">
        <h2 class="modal-title">${props.title}</h2>
        <div class="modal-footer">${props.footer}</div>
    </div>
  `;
}

innerHTML(document.body, html`<${MyComponent} title="Test Modal Dialog" footer="<button class='btn-close>Close</button><button class='btn-cancel' onClick=${() => this.remove()}>" />`);

The footer props is dynamic. Is it possible without using web-component?

You should be able to pass like:

<${MyComponent}
  title="Test Modal Dialog"
  footer=${html`<button class='btn-close>Close</button><button class='btn-cancel' onClick=${() => this.remove()}>Cancel</button>`}
/>

Live demo here: https://glitch.com/edit/#!/handy-silky-fortnight

Hi @tbranyen,
Can I use componentWillMount in function component?
Thank you

@Dan-Do that will be achieved with the createSideEffect method. Stateful function components are still in progress, but I can add this for you. Are there any other React-like function component features you'd like to see added?

Also to be clear this will only map componentDidMount as componentWillMount is only available in class components.

I'm thinking of using the following signature:

import { html } from 'https://diffhtml.org/core';
import { createSideEffect } from 'https://diffhtml.org/components';

function Component() {
  // componentWillUpdate stuff happens in the render function

  createSideEffect(() => {
    // componentDidMount/Update
    return () => {
      // componentDidUnmount
    };
    // I hesitate to add this array of dependencies to skip re-render, but it is handy.
  }, [someVariable]);

  return html`Some component markup`;
}

@Dan-Do that will be achieved with the createSideEffect method. Stateful function components are still in progress, but I can add this for you. Are there any other React-like function component features you'd like to see added?

Hi @tbranyen I am new to SPA, and I haven't looked at React so I don't know it's features.
For this small request, I just want to fetch data using async/await then render, then attach to the DOM. I tested the following code but it repeat forever:

function MyComponent(props) {
  const getData = function() {//fetch here}
  //call getData before return the html
  getData(...)
  return html`
    <div>show ${data} here</div>
    <button onclick=${getData(...)}> Refresh </button>
  `;//pseudo code just to explain my idea
}

I will try createSideEffect and get back later.

It seems the version of unpkg.com is different from the master brand. This line is currently in unpkg, which upgrades function to class component

// Function component, upgrade to a class to make it reactive.

but master brand is renderedTree = createTree(Component(props));

I am using ES6 module in browser

import { createSideEffect } from 'https://unpkg.com/diffhtml-components?module';

Error: Uncaught SyntaxError: import not found: createSideEffect

Hi @Dan-Do I have not yet added the createSideEffect method. I will put it in today, I think that will suffice for your needs.

@tbranyen I setup a codepen to test
https://codepen.io/ali33yukisakura/pen/JjOodXK?editors=1011
function View1(props) {...}
But it doesn't work. May be I used it wrong way :(

Checking out your code ๐Ÿ‘€, I can see a few issues:

The major one is that I didn't add a unit test to ensure both createState and createSideEffect work together. There was a bug which I have fixed and will release a new version.

I also noticed that you were using response.text() instead of response.json(), I have fixed that and cleaned up the component below. I tested this works with my latest fixes. Once I publish the new version and you make the response.json() fix, your code should work as expected.

      function View1(props) {
        const [state, setState] = createState({ isLoading: false, posts: [] });

        createSideEffect(async () => {
          setState({ ...state, isLoading: true });

          const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
            method: 'GET',
            credentials: 'same-origin',
            headers: {'Accept': 'application/json, text/javascript'},
          });

          setState({ ...state, isLoading: false, posts: await response.json() });
        });

        return html`
          <div class="view">
              <h2>${props.title}</h2>

              ${state.isLoading
                ? html`loading...`
                : html`${state.posts.map((post) => html`<h3>${post.id} - ${post.title}</h3>`)}`
              }
          </div>
        `;
      }

New version 1.0.0-beta.26 contains the fix for the aforementioned bug.

New version 1.0.0-beta.26 contains the fix for the aforementioned bug.

Thanks @tbranyen it works nice. I will come back if finding any issue.

Hi @tbranyen Just notice that you released version beta-27 which made it not work anymore.
I added more html to the codepen, link
https://codepen.io/ali33yukisakura/pen/ZEvXNeZ?editors=1011

I can see a bug in the code that prevents deeply nested components from getting mount/unmount lifecycle events. I've updated the unit tests and will have a fix out by this weekend. Thanks for your patience, this is really helpful having real usage with the components layer.

@Dan-Do I've refactored how lifecycle events are fired and it appears way more stable now since I cache exact component instances to trigger mount on vs crawling to find them.

When I update your codepen to use https://diffhtml.org/components instead of unpkg it works great. Can you try this for a bit and let me know what you think? Once you're happy with it I'll promote out a new version and you can go back to using unpkg.

@tbranyen when I use
import { createState, createSideEffect } from 'https://diffhtml.org/components';
it said
SyntaxError: import not found: createSideEffect

Edit: this error does not happen on my localhost, just on codepen
https://codepen.io/ali33yukisakura/pen/ZEvXNeZ?editors=1011

Edit2: on my localhost, the createSideEffect does not work.

@Dan-Do probably a cache related issue. Can you fully wipe the browser cache or test your codepen in incognito mode?

Hi @tbranyen It works now.

However I had an issue on my localhost but I cannot reproduce it on codepen.

I have a ViewMain

function ViewMain(props) {
  return html`
  <div id="view-main" class=${activeRoute === props.route ? "active" : "hidden"}>
    <${CompForeignerStatistic} route="/foreigner/statistic" />
    <div class="modal fade"></div>
  </div>`;
}

The CompForeignerStatistic uses createSideEffect we discuss so far.
It messed when rendered

<div id="view-main" class="modal fade" route="/foreigner/statistic" div=""></div>

If I remove this line <div class="modal fade"></div> then it's rendered ok.
Do you have any idea where to investigate?

Looks like something weird with the HTML parser, where it's treating the adjacent div like attributes. Will see if I can reproduce the issue.

The codepen you shared before has similar code which looks fine:
image

Can you share the code that includes CompForeignerStatistic?

I made a reduced test case and can't reproduce there either:

<main id="main"></main>

<script type="module">
import { html, innerHTML } from 'https://diffhtml.org/core';
import { createSideEffect} from 'https://diffhtml.org/components';

const activeRoute = 'test';

function CompForeignerStatistic() {
  createSideEffect(() => {
    console.log('mounted');
  });

  return html`<div>testing</div>`;
}

function ViewMain(props) {
  return html`
  <div id="view-main" class=${activeRoute === props.route ? "active" : "hidden"}>
    <${CompForeignerStatistic} route="/foreigner/statistic" />
    <div class="modal fade"></div>
  </div>`;
}

innerHTML(main, html`<${ViewMain} route="test" />`);
</script>

image

@tbranyen I know what makes that error.
Please visit the codepen, change this

      function View1(props) {
        return html`
        <div id="view-main" class=${activeRoute===props.route?"active":"hidden"}>
          <${Test} fake="true" />
          <div class="success">Hello ๐Ÿ‘จ OK!</div>
        </div>`;
      }

by removing the line break in html like this:

      function View1(props) {
        return html`
        <div id="view-main" class=${activeRoute===props.route?"active":"hidden"}>
          <${Test} fake="true" /><div class="success">Hello ๐Ÿ‘จ OK!</div>
        </div>`;
      }

Then the <div class="success">Hello ๐Ÿ‘จ OK!</div> won't be rendered.

Awesome, I can reproduce this bug. I will put in a fix asap.

So the reason this is happening is that I added support for special HTML characters <>/ inside attribute values, which is messing up the parsing. I've got a solution locally that works excellent, by first separating out the individual tags, and then parsing the internals.

I think it'll be ready today. So for now you could break up your markup to separate lines to avoid any bugs, and once I get this fix in, it should be working as expected with single lines.

image

This regex appears plenty sufficient to chunk out the tags, and then I'm going to use the existing regex to parse out the tagName and attributes.