preactjs / preact-custom-element

Wrap your component up as a custom element
MIT License
360 stars 52 forks source link

Slots fail to pass as props when element is created after register() is called #81

Closed effulgentsia closed 3 months ago

effulgentsia commented 1 year ago

If I follow the example in https://preactjs.com/guide/v10/web-components/#passing-slots-as-props and bundle the jsx file into an iife (e.g., via ./node_modules/.bin/esbuild test-src.jsx --bundle --jsx-import-source=preact --jsx=automatic --minify --outfile=test.js), then the example works as documented if my script runs after the element such as:

<text-section>
  <span slot="heading">Nice heading</span>
  <span slot="content">Great content</span>
</text-section>
<script src="test.js"></script>

However, the heading and content props are undefined if my script runs before the element such as:

<script src="test.js"></script>
<text-section>
  <span slot="heading">Nice heading</span>
  <span slot="content">Great content</span>
</text-section>
effulgentsia commented 1 year ago

Additional info...

This FAILS (meaning, the slots DON'T pass as props and therefore aren't rendered):

<html>
<head><script src="test.js"></script></head>
<body><div id="wrapper">
  <text-section>
    <span slot="heading">Nice heading</span>
    <span slot="content">Great content</span>
  </text-section>
</div></body>
</html>

This also FAILS:

<html>
<head><script src="test.js"></script></head>
<body><div id="wrapper">
  <script>
    document.write(`
      <text-section>
        <span slot="heading">Nice heading</span>
        <span slot="content">Great content</span>
      </text-section>
    `)
  </script>
</div></body>
</html>

However, this PASSES (meaning, the slots DO get passed as props and are rendered):

<html>
<head><script src="test.js"></script></head>
<body>
  <div id="wrapper"></div>
  <script>
    wrapper.innerHTML = `
      <text-section>
        <span slot="heading">Nice heading</span>
        <span slot="content">Great content</span>
      </text-section>
    `
  </script>
</body>
</html>
blopker commented 11 months ago

I've just run into this as well when I took defer out of the script tag in my head section. To be fair, I don't need it at this point since all my custom elements are there when the page loads, but this is surprising behavior. Aside from slots, everything else seemed to work.

Edit: For completeness, I'm using { shadow: false } when registering. Edit2: The issue is the same for { shadow: true }

blopker commented 11 months ago

Looks like this is a documented issue with Custom Elements: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements

Note that in this case we must ensure that the script defining our custom element is executed after the DOM has been fully parsed, because connectedCallback() is called as soon as the expanding list is added to the DOM, and at that point its children have not been added yet, so the querySelectorAll() calls will not find any items. One way to ensure this is to add the defer attribute to the line that includes the script.

Looks like if the Custom Element is added to the DOM after the first pass then it works fine. The issue is when an element is already registered, then connectedCallback is triggered immediately when the Custom Element tag is found, but before the children are parsed. This is why setting innerHTML on an element that was already parsed works, but using document.write to append a CE before the first DOM pass happens, fails.

Since we should all be using defer in our script tags to avoid blocking the DOM anyway, this feels like a non-issue to me. Maybe it's worth documenting though? One suggestion I saw was to put a setTimeout(0) in connectedCallback to slightly defer until the element's children were parsed, but I'm not sure if that actually works.

rschristian commented 3 months ago

Sorry for getting to this so late, but yeah, it's a bit of a fundamental issue (or feature, depending on how you need to use it I suppose) of custom elements.

If someone wanted to add a note somewhere, we'd probably accept it, but how you use your scripts generally falls outside the scope of the library here.