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

How can a custom element detect when it is distributed, and what its composed parent is? #941

Open
trusktr opened this issue Sep 10, 2021 · 2 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Sep 10, 2021

This is similar to #504. That question was based around the v0 API, but the question is still valid in the v1 API: there's no easy way to do this, apart from the composed parent having to be a custom element that can cooperate and notify a distributed child, which is not possible with builtins (if a custom element is distributed via slot to a div element, that div element is not going to notify the distributed child that the div is the composed parent).

I already do this fine with cooperating elements. For example, this works:

<script src="elements/x-foo.js"></script>
<body>
  <x-foo id="outside"></x-foo>
</body>
<script>
const root = document.body.attachShadow({mode: 'open'})
root.innerHTML = `
  <x-foo id="inside"><slot></slot></x-foo>
`
</script>

In that example, the inside x-foo element can observe its slot, and when inside sees that an x-foo element is distributed to it (the outside x-foo element in this case), the inside x-foo can notify the outside x-foo that the inside x-foo is the outside x-foo's composed parent (in the conceptual composed tree).

However, here is a use case where that isn't possible when inside is not a cooperating custom element. Suppose that x-foo is supposed to render custom graphics (with canvas for example) and the result should be based on the composed parent's size (this is something that CSS rendering already already does, CSS can render features of an element based on the element's composed parent's size, but now we're trying to implement our own rendering system).

Suppose we now have this example:

<script src="elements/x-foo.js"></script>
<body>
  <x-foo id="outside"></x-foo>
</body>
<script>
const root = document.body.attachShadow({mode: 'open'})
root.innerHTML = `
  <div><slot></slot></div>
`
</script>

In that example the x-foo will be distributed to the div element. However, there's no way for the x-foo to know that it was distributed, or to which node it was distributed to, therefore the x-foo can not know what its composed parent is, and can therefore not know what the composed parent size is for its canvas rendering.

This is solvable if we do something hacky: we could make a loop (f.e. using requestAnimationFrame) in the x-foo implementation to continually check what the composed parent is, but this is obviously not ideal (it needlessly uses device battery, for example). Each time we poll, we can check outside.assignedSlot. ... .asignedSlot.parentElement. There's just not way to know when outside.assignedSlot has changed, therefore also no way to check for outside.assignedSlot. ... .assignedSlot to find the final composed parent, apart from polling or other hacks.

With some imagination, another complicated way of doing this could be to monkey patch attachShadow, and use MutationObserver in all trees to detect slots and notify the x-foo of its composed parent (or just the parent size).

@trusktr
Copy link
Contributor Author

trusktr commented Sep 10, 2021

Maybe a better title is "mechanism for observing composed connections".

In my project that has cooperating custom elements, I've implemented things like childComposedCallback, childUncomposedCallback, parentComposedCallback, etc, which what you might think. However, these fall apart when one of my elements is distributed into a non-cooperating element such as a non-custom element.

If the Custom Elements API would provide something like a parentComposedCallback, this would notify a custom element that the element's composed parent has changed (f.e. because a slot attribute was set onto the element, the element was added as immediate child of a ShadowRoot, etc, any condition that causes the final composed parent to change). Example:

class MyEl extends HTMLElement {
  // These fire any time the composed parent changes (f.e. distribution to a different slot, being added to a ShadowRoot, etc)
  parentComposedCallback(composedParent) {
    console.log('New composed parent:', composedParent)
  }
  parentUncomposedCallback(previousComposedParent) {
    console.log('Previous composed parent:', previousComposedParent)
  }

  // A similar thing with composed children:
  childComposedCallback(composedChild) {
    console.log('child composed:', composedChild)
  }
  childUncomposedCallback(uncomposedChild) {
    console.log('child uncomposed:', uncomposedChild)
  }
}

Currently, childComposedCallback and childUncomposedCallback are polyfillable using slotchange and assignedElements, but polyfilling parentComposedCallback is more difficult, and would require patching attachShadow and then using MutationObserver in ShadowRoots to detect final distribution locations.

Having childConnectedCallback and childDisconnectedCallback would be useful for custom cases, for example custom rendering pipelines (f.e. with WebGL) that require knowledge of the composed tree in order to render the proper hierarchy on screen.

Related helpful new web APIs that a custom element could take advantage of could be node.composedParent (in which case we could skip having a parameter for parentComposedCallback), node.composedChildren (list of Elements), and node.composedChildNodes (list of Nodes). Perhaps these new APIs would return null or empty arrays if ShadowRoot are created with mode:closed, although in that case they wouldn't be useful for a custom rendering system that needs to be able to understand the composed tree regardless if roots are open or closed.

Are there any other ways we could allow custom elements take advantage of the composed tree shape in scenarios when they have custom rendering implementations?

Example: in my case, LUME, the internal Three.js tree managed by LUME custom elements match in shape with the composed tree so that the rendering outcome is as expected.

@trusktr
Copy link
Contributor Author

trusktr commented Apr 4, 2022

I updated my previous code sample to delete incorrect code comments.

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

No branches or pull requests

1 participant