preactjs / preact-custom-element

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

Allow static children in shadow: false mode #56

Open jvdsande opened 3 years ago

jvdsande commented 3 years ago

What does this PR do

This PR allows passing children for elements even with shadow: false. It only works for first render, if the children are modified after first render (i.e. if the element is used in a framework such as LitElement), children are not updated.

It also cleans up the overall usage with shadow: false by getting rid of the <slot> wrapper element, attaching the context event listener on the custom element itself.

Why is this PR needed

It partly fixes #41. Children are a big part of a component design, not being able to pass children is a strong limitation. As of now, passing children results in them being duplicated. With this PR, at least the initial rendering is correct. It also enables internally handled children to be hydrated when using SSR.

Taking the following example:

function MyComponent({ children }) {
  return (
    <div class="children-go-here">
       {children}
    </div>
  )
}

register(MyComponent, 'my-component', [], { shadow: false })
<my-component>Hello World!</my-component>

The resulting markup without this PR is:

<my-component>
   <!-- correctly rendered component -->
   <div class="children-go-here">
      Hello World!
   </div>
   <!-- incorrectly kept initial children -->
   Hello World!
</my-component>

With this PR, it becomes:

<my-component>
   <!-- correctly rendered component -->
   <div class="children-go-here">
      Hello World!
   </div>
   <!-- correctly cleaned-up initial children -->
</my-component>

Implementation details

This PR touches at various points of the wrapper's lifecycle. The first breaking change here is the addition of the following in toVdom:

    // Remove all children from the topmost node in non-shadow mode
    if (!shadow && nodeName) {
        element.innerHTML = '';
    }

This snippet removes all children present in the original element before rendering (and after having copied them to the vDom's children), thus removing the duplicated/wrongly placed children.

The PR also removes the manual call to connectedCallback when handling props update before initial render. In place, it stores the props in the _props temporary variable, so that they are applied at first render. This allows to avoid a bug where the component is rendered inside itself when connectedCallback is called multiple time.

The rest is implementation details about removing the <slot> element when not using ShadowDOM, has it is not needed.

Misc

Removing the <slot> wrapper when not using ShadowDOM allows for a cleaner DOM, but it does come at a cost: we loose the ability to unregister the _preact event listener. We could add this ability back by using a useEffect hook in the Slot component, but I did not want to add a dependency on preact/hooks there, since the library seems to avoid it.

The usage is also a bit strange since modifying the children of the custom elements will not update the children of the Component, since it does not call attributeChangedCallback. I've not found a way around this.

Finally, maybe handling children with non ShadowDOM is not something you want to be allowed, in which case this PR can just be closed 🙂