sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.99k stars 4.25k forks source link

Component-patterns with web-components (v4) #8690

Closed KenAJoh closed 1 year ago

KenAJoh commented 1 year ago

Describe the problem

The new changes made to custom-elements šŸŽ‰ changed how get_current_component works, and with it comes some changes to how one create a component-api with purely web-components. (as far as my current testing has lead me to believe, but might be mistaken here)

A constant hassle when working with shadow dom is how one passes values between the shadow-dom, and in v3 this could be "solved" by using get_current_component().shadowRoot to access both parent and childrens within slots. This allowed the following component-pattern:

<wc-accordion>     // <- shadowroot open
    <wc-accordion-item>   //  <- shadowroot open
        <wc-accordion-heading>   // <- shadowroot open
            Heading text
        </wc-accordion-heading>    
        <wc-accordion-content>   // <- shadowroot open
            Content text
        </wc-accordion-content>
    </wc-accordion-item>
</wc-accordion>

where accordion-item stored and passed both the "open"-state and the "ontoggle"-handler down to accordion-heading and accordion-content. This "worked", but was not pain-free...

One could sort-of solve this now by using the following pattern:

<wc-accordion>
    <wc-accordion-item heading="heading-text">
            content
    </wc-accordion-item>
</wc-accordion>
// or
<wc-accordion>
    <wc-accordion-item >
           <span slotname="heading">heading</span>
           <div slotname="content">content</div>
    </wc-accordion-item>
</wc-accordion>

But that doesn't solve the core issue...

One could dispatch events from accordion-header up to item, following the 'properties down, events up'-pattern, but what is the expected and preferred way to pass the open-prop down across the nested shadow-dom? This closed issue adds context-support for web-components, possibly solving this. But so far with 4.0.0-next.0 i have yet to get this working (can set context, but getContext is always undefined)

What i'm getting to is: How does Sveltes custom-elements want and expect a good component-pattern to look and work with 2-way communication between components?

Describe the proposed solution

Stencil solves this by making it possible to get and change child web-components with querySelector directly on the parent element even its if its nested shadow-doms

this.accordionHeader = this.el.querySelector("wc-accordion-header");
this.accordionHeader.open = this.open;

How this is implemented i'm not aware of, and might only be possible with virtual-dom (as stencil uses)

Lit solves this with giving access to this.children or this.defaultNodes (not 100% on defaultNodes, but thats how spectrum web-components does it)

Alternatives considered

In v4 one could disable shadow-dom for the nested elements, and only use shadow-dom on the wrapper. This could a viable pattern, but have yet to do any testing with it for now. accordion-item will still be outside the wrappers shadow-dom, so not viable.

The easiest way i have found for now is to make queries on the document, but this adds a different complexity with checking for the correct dom-element and adding unique identifiers to each component instance.

This issue under the 4.x milestone is related and would solve this to a certain degree. Found a PR on this, but its 3y old now..

Importance

would make my life easier

dummdidumm commented 1 year ago

To clarify: The issue is that you want to create web components that should be used together in a way that these components know of their children so they can put them into specific slots without you having to explicitly define those slots as a user?

KenAJoh commented 1 year ago

A potential solution would as a side-effect allow this to be done i guess. My main issue is how to access and manipulate state of child web-components

// html
<wc-accordion>
    <wc-accordion-item>
        ...
    </wc-accordion-item>
</wc-accordion>
// svelte code (wc-accordion)
<script>
    let ref: HTMLDivElement;

    onMount(() => {
        ref.querySelector("wc-accordion-item") // <- null
        ref.parentElement  // <- null
        ref.shadowRoot  // <- null
    });
</script>

<div bind:this={ref}>
    <slot />
</div>
// dom
<wc-accordion>
    #shadow-root (open)
        <div></div>        <- Can only access this
    <wc-accordion-item>    <- and not this since its outside shadow-root
        ...
    </wc-accordion-item>
</wc-accordion>

Making it hard to pass state down to all children like this

<wc-accordion variant="neutral">
--
const item = ref.querySelector("wc-accordion-item");
item.variant = "neutral"

while it makes sense that you cant access elements outside the shadow-root with querySelector directly on the element, having access to either parentElement or shadowRoot would potentially open up an easier way to do this.

dummdidumm commented 1 year ago

Use getRootNode and host, then you can query the other elements: ref.getRootNode().host.querySelector('wc-accordion-item').

Given that this is now possible using public APIs (the previous version with get_current_component is merely a hack using internal APIs which is brittle) I'm marking this as a docs issue - not sure where in the new docs to put it yet.

KenAJoh commented 1 year ago

Thanks for the help! ref.getRootNode().host did the trick for us āœ”ļø Getting som ts-errors with host: Property 'host' does not exist on type 'Node'., but closing this issue as completed as it still works.

patricknelson commented 1 year ago

The easiest way i have found for now is to make queries on the document, but this adds a different complexity with checking for the correct dom-element and adding unique identifiers to each component instance.

I concur; ideally you would just stay entirely in Svelte to intuitively get/set context and could just compose your custom elements any way you want. The goal would be for context to "just work" in the child element, as long as it is nested somehow under the parent element.

Like... this: https://svelte.dev/repl/b404a1addaf348eabcff3f6089707297?version=4.2.1

The advantage of using svelte-retag in this way is that you also have plain Svelte components as well... like this: https://svelte.dev/repl/7a26be30148644b6895469274bb69a32?version=4.2.1

To me this is a pretty useful pattern! Check out svelte-retag if you want to try this approach to get context working cleanly in custom elements. Also, check out this tab-based demo here too: https://svelte-retag.vercel.app/

Edit: p.s. As you can see in the first demo above, you can also incorporate the custom elements directly into Svelte components too (note the <custom-parent> included in the App.svelte file); this is a native feature of Svelte anyway. So... you can still mix and match it as you see fit, which I also think is pretty cool.