Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context not being passed to parent slot consumer #4618

Open
Arthurdw opened this issue Apr 11, 2024 · 8 comments
Open

Context not being passed to parent slot consumer #4618

Arthurdw opened this issue Apr 11, 2024 · 8 comments

Comments

@Arthurdw
Copy link

Which package(s) are affected?

Lit Core (lit / lit-html / lit-element / reactive-element), Context (@lit/context)

Description

When a child element provides a context for a parent slot this doesn't properly pass through. (see demo for better explanation)

Reproduction

Lit Playground - no proper passthrough demo
Vue Playground - expected behavior

Workaround

I have not found a workaround (besides from not having child elements providing parent slotted children context, but this is extremely limiting)

Is this a regression?

No or unsure. This never worked, or I haven't tried before.

Affected versions

3.1.2

Browser/OS/Node environment

Google Chrome: 123.0.6312.106 (Official Build) (64-bit) (cohort: Stable)
Node: v20.11.1

I believe this issue affects all browsers (not tested)

@Arthurdw
Copy link
Author

PS: because you can not pass parameters into slot definitions this limitation makes it to my understanding impossible for child components to provide things to a slot which is defined by the parent.

@justinfagnani
Copy link
Collaborator

This is caused by a timing issue related to Lit's asynchronous rendering. Lit elements render themselves in a microtask, so they can gather all the state updated in a single batch and render just one.

This means that a parent element will render its children, the children will be connected, then the children will render its children, and so on.

So what's happening here is that the render of <slotted-context-demo> produces a tree like:

<slotted-context-demo>
  #shadow-root
    <my-child-parent-wrapper>
      <my-child-consumer></my-child-consumer>
    </my-child-parent-wrapper>

Then a later microtask renders <my-child-parent-wrapper>, which introduces it's shadow root and the tree becomes this:

<slotted-context-demo>
  #shadow-root
    <my-child-parent-wrapper>
      #shadow-root
        <my-child-provider name="Sample">
          <slot><!-- <my-child-consumer> is projected here --></slot>
        </my-child-provider>
      <my-child-consumer></my-child-consumer>
    </my-child-parent-wrapper>

More rendering of <my-child-consumer> and <my-child-provider> fill out the tree, but this is the step that puts <my-child-consumer> into the "flattened" tree of <my-child-provider> so that composed events fired from <my-child-consumer> bubble up through <my-child-provider> and the context works.

What's happening with your demo is that the context event is being fired before <my-child-parent-wrapper> is rendering, when the tree has that first shape. So there's no </my-child-provider> to receive the context event.

This normally isn't an issue for elements that provide to either their shadow DOM children, because they are always created after the provider. But it can be an issue when providing to light-DOM children. Either in this slotted case, or without slots if the child is upgraded before the parent so that the parent doesn't have a chance to set up it's context event listeners yet.

To address this issue we've added a helper called ContextRoot (https://lit.dev/docs/data/context/#contextroot) that can catch and re-fire context events if a new provider appears in the tree. You can add the ContextRoot to the root of the document or any element, like your demo element. Then your consumers need to subscribe to the context so that they can get new values.

export class SlottedContextDemo extends LitElement {
  constructor() {
    super();
    const contextRoot = new ContextRoot();
    contextRoot.attach(this);
  }

See the updated demo: https://lit.dev/playground/#gist=faeb76db567f94b8dfcf40d34e3e42bc

@Arthurdw
Copy link
Author

This is caused by a timing issue related to Lit's asynchron...

Thank you for the quick and detailed explanation, I now understand why this happens and how it can be resolved.

However I do feel like something like this should be more explained on the lit site docs (exactly like how you wrote it here) to prevent future confusions like this one.

@augustjk
Copy link
Member

Thanks for raising this. I agree this seemingly unexpected behavior is not clear in the docs, especially with how slotting timing works. We had some discussion sparked by this in yesterday's eng meeting and are considering making context roots be auto applied to the document/window rather than manual.

@oscarmarina
Copy link

Hi all,
@Arthurdw I'm experimenting with a controller that includes both ContextProvider and ContextConsumer. Placing the consumer after the first update could prevent context errors by ensuring it receives the context immediately. This delay in placement might be useful in this scenario, although it may not work for all cases.

In the original Lit example, when moving a <my-section> node manually, with ChromeDevTools, it doesn't update correctly, but it seems to do so correctly this way.

TS Playground:

JS Playground:

==
image

@Arthurdw
Copy link
Author

Hey @oscarmarina,

I hope I'm understanding this correctly, but it seems like your parent also needs to define the context consumer, even if it's not being utilized? I gave it a shot with the sample, but unfortunately didn't have any luck.

@oscarmarina
Copy link

Hey @Arthurdw, what I meant is that another option could be to simply wait for the DOM to be rendered initially.

See (async connectedCallback):
https://lit.dev/playground/#gist=393bcd6cf1c9b3011ac86db5f6148516


Another approach to utilizing context within a component is to directly employ the ContextConsumer controller

@Arthurdw
Copy link
Author

See (async connectedCallback): https://lit.dev/playground/#gist=393bcd6cf1c9b3011ac86db5f6148516

All right, I understand now. This has helped to clarify things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

No branches or pull requests

4 participants