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 transcluded into a shadow tree (slotchange)? #504

Closed
trusktr opened this issue May 22, 2016 · 48 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented May 22, 2016

(I asked a related question on StackOverflow, in case that helps reach different people knowledgeable on the same topic.)

I made some custom elements for creating 3D scenes, for example:

<motor-scene id="motor-scene">
    <motor-node
        absoluteSize='200,200,0'
        rotation='0,30,0'
        align='0.5,0.5,0'
        mountPoint='0.5,0.5,0' >

        <h1>{props.msg}</h1>

    </motor-node>
</motor-scene>

I noticed attachedCallback fires only when attached to the light tree (in Chrome). It breaks when I try to transclude things together (using <content>), for example:

shadowDOM inside of a <foo-bar> custom element:

            <div>
                <motor-scene id="motor-scene">
                    <content selector="motor-node">
                    </content>
                </motor-scene>
            </div>

<foo-bar> usage:

    <foo-bar>
        <motor-node
            absoluteSize='200,200,0'
            rotation='0,30,0'
            align='0.5,0.5,0'
            mountPoint='0.5,0.5,0' >

            <h1>{props.msg}</h1>

        </motor-node>
    </foo-bar>

where foo-bar is defined like this:

document.registerElement('foo-bar', class FooBar extends HTMLElement {
    createdCallback() {
        this.shadowRoot = this.createShadowRoot()
        this.shadowRoot.innerHTML = `
            <div>
                <motor-scene id="motor-scene">
                    <content selector="motor-node">
                    </content>
                </motor-scene>
            </div>
        `
    }
})

The simple example shows the transclusion. As you can see, the expected is that the motor-node is placed inside the motor-scene (which is a requirement for things to work properly (preserve-3d, CSS transform caching, etc).

I noticed that the attachedCallback will be called when the motor-node exists in the light DOM, but not when it is put into it's new place. I guess this makes sense in the perspective of a component user who it attaching the motor-node into a some-layout for example, but for me (the component dev), I've stumbled, because my custom elements rely on their attachedCallbacks in order to set themselves up: motor-nodes can only be children of motor-scene or other motor-node elements, and this is all detected with attachedCallback (and throws an error if this isn't the case). So, it seems like this design has shot me in the foot when it comes time to try and transclude things the web-components way.

In React, the same concept that I am trying to achieve would work just fine, because things in DOM are constructed for real in a single light DOM (so all the attaching/detaching) works just fine.

Unless I'm not seeing the full picture, it seems that if I want to make this work with Web Components that I need to have a component system that sites purely on top of the light DOM and not use shadow DOM (for example, React, or just Custom Elements but without using Shadow DOM inside them).

It also seems that if I want to allow my end users to use Shadow DOM to construct their own trees using my custom elements (which is a possibility), that I might have to have my own shadow wherein my entire scene graph is contained (so it behaves like a light DOM as far as I'm concerned inside of a scene's shadow root), and that I would need to be able to traverse all of the user's shadow dom roots in order to detect and construct a scene graph in my own shadow root. (EDIT, for traversing, if I end up needing to do that, I suppose I can recommend the user to set the ShadowRootMode to open, but let's first try to avoid that).

Basically, this seems like a huge undertaking that is almost completely not worth trying (compared to using something else like React). It seems like if we modified transclusion to behave like attaching (from light DOM) and re-attaching (into shadow DOM) along with firing attached/detachedCallback in those cases would let me do what I want to do, because those callbacks trigger calls that construct a tree structure parallel to the DOM structure (I would have to ignore the <content> elements, and besides warning about "motor-nodes need to be attached to motor-scene or motor-node elementsI would then also be able to warn about "motor-nodes can only be trancluded into motor-scene or motor-node elements". In React, we don't have to detect the transclusion case (as far as Custom Elements are concerned), and I would also argue Shadow DOM is not needed when making components with React but some people use it anyways (which I think is unnecessary extra complexity).

To show what my elements are doing visually, basically the attachedCallbacks are used to create a parallel tree structure where the os are the motor-scene and motor-node elements:

       light DOM            parallel tree structure

            o                     o
            |                     |
            |                     |
       o----o----o    -->    o----o----o
            |    |                |    |
            o    |                o    |
                 o                     o

With transclusiong, this strategy for creating the behind-the-scenes tree structure is falling apart. I'll need the structure in order to be able to use that in WebGL (not just DOM). If I ignore WebGL, then I can get rid of my behind-the-scenes tree structure, and let DOM handle's it's own 3D scene graph entirely. Instead of holding matrix transforms in the behind-the-scenes structure, I would just place them right inside the custom element instances and be done with it. Simple.

I guess I'm just trying to figure out how to use transclusion the web components way. If I stick to React, the problem is easily solved: just let React attach all elements into light DOM, and I'm done with it. But, I don't want to settle with a solution like that then not be able to use my components in an Angular app (suppose I switch to a different project with new framework requirements, then I'd have to port the components or start from scratch). I want to truly write once, use everywhere.

Sorry that this is all somewhat random order, because I'm thinking as I go. A-Frame will have similar problems. I'll also ask there to see if anyone has thought about this.

END GOAL: I want to build with WebComponents, not with a 3rd party lib like React, so that my elements can be used anywhere (React, Angular, Meteor, Ember, etc)

@trusktr
Copy link
Contributor Author

trusktr commented May 22, 2016

I'm not sure what a good title for this issue is.

@trusktr trusktr changed the title attachedCallback in light tree, but what about in shadow tree? attachedCallback in light tree, but when transcluded into shadow tree? May 22, 2016
@trusktr
Copy link
Contributor Author

trusktr commented May 22, 2016

Updated the title. Maybe we need something like a slottedCallback or something. Is anything like that planned? That would alleviate my problem immensely (I think). I would need to use attachedCallback and slottedCallback in tandem in order to detect when the user is using light DOM, and when the user is using shadowDOM transclusion. For example, if I detect that a <motor-node> is not attached to a <motor-scene> or <motor-node> in attachedCallback (which fires in the DOM where the element appears before transclusion), then I can make an assumption that the element might possibly be transcluded into a shadow DOM and I can be ready to detect that case.

Is there any current/recommendable way of detecting when an element has been transcluded?

@trusktr
Copy link
Contributor Author

trusktr commented May 22, 2016

Where can I find exactly what the open and closed states are used for in Shadow DOM? I already looked here, but that doesn't help much.

@trusktr trusktr changed the title attachedCallback in light tree, but when transcluded into shadow tree? How to detect when an element is transcluded into a shadow tree? May 22, 2016
@treshugart
Copy link

@trusktr there's a slotchange event being implemented in v1 that is a non-bubbling event fired on slot elements when the slotting algorithm runs. More discussion in #288. Would this solve your issue? I noticed the web component API you're using is the one Blink had prior to v1. AFAIK there's no way to detect this in "v0" without using a Mutation Observer on your <content> elements.

@hayatoito
Copy link
Contributor

Correct. There is no easy way in v0.

e.g. Polymer folks had to spend tremendous efforts to detect such a change in v0.
That is the reason why they really want slotchange event in v1.

Blink is actively implementing slotchange events for v1.

@treshugart
Copy link

treshugart commented May 26, 2016

@trusktr If you want a partial polyfill that supports a slotchange event, have a look at https://github.com/skatejs/named-slots/. You can try enabling Shadow DOM V1 support in Chrome Canary using:

/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --enable-blink-features=ShadowDOMV1

Though, I'm not sure if it's been implemented there yet.

@hayatoito
Copy link
Contributor

Regarding the support of slotchange events in Blink, I am now implementing it at https://codereview.chromium.org/1995203002

@trusktr
Copy link
Contributor Author

trusktr commented May 31, 2016

@hayatoito

Regarding the support of slotchange events in Blink, I am now implementing it at https://codereview.chromium.org/1995203002

Awesome!

@treshugart

there's a slotchange event being implemented in v1 that is a non-bubbling event fired on slot elements when the slotting algorithm runs.

This sounds nice, but I think that this event will be easy for the owner of a shadow root to use, but what about elements that are being inserted into a slot? I think (please let me know if I'm wrong) that due to Shadow DOM encapsulation that some (potentially custom) element being distributed into a slot will have no idea what slot that is, and in my case, the custom elements that I'm making need to know where their slots are located and what the parent element of the slot is. Is there a way for the slotchange event to be useful to my custom elements (not just to the shadow root owner)?

@trusktr trusktr changed the title How to detect when an element is transcluded into a shadow tree? How can a custom element detect when it is transcluded into a shadow tree? May 31, 2016
@treshugart
Copy link

and in my case, the custom elements that I'm making need to know where their slots are located and what the parent element of the slot is

I can't really answer without knowing your use-case, but I've found that you should be emitting events that parents respond to. Reaching up in the DOM tree creates a tight coupling of your child component's functionality to the DOM structure in which it's placed defeating the purpose of modularity and componentisation.

@hayatoito
Copy link
Contributor

I think (please let me know if I'm wrong) that due to Shadow DOM encapsulation that some (potentially custom) element being distributed into a slot will have no idea what slot that is,

You can use Element.assignedSlot: http://w3c.github.io/webcomponents/spec/shadow/#widl-Element-assignedSlot

@trusktr
Copy link
Contributor Author

trusktr commented Jun 3, 2016

@hayatoito

On getting, the attribute must return the assigned slot of the context object, if there is, and the assigned slot's root's mode is "open". Otherwise must return null.

What if it's closed? My problem is that to construct an efficient WebGL tree from my HTML interface, I need to mirror the flat tree if I want the WebGL portion to match with it. But at the same time, I can't tell ever developer to leave their shadow root open, and nor can one who is merely re-using components make the guarantee about all the components they use having open shadow trees.

@hayatoito
Copy link
Contributor

Yeah, no way for a closed mode. We have to honor the intention of a component author who chooses "closed" intentionally.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 3, 2016

So slot-containing elements inside a shadow tree can still see what nodes are distributed into their slots (on slotchange) even if that shadow tree is closed?

If so, I can modify my API to have parent nodes look at their child nodes (or look at the distributed nodes of their slots) in order to create the behind-the-scenes flat-tree mirror that I need.

@rniwa
Copy link
Collaborator

rniwa commented Jun 4, 2016

I don't think you can create a flat tree mirror for closed shadow trees because all those slots that may have distributed contents would not be accessible from your code. You may need to do something like overriding Element.prototype.attachShadow and intercepting all the calls to that function.

@hayatoito
Copy link
Contributor

hayatoito commented Jun 6, 2016

So slot-containing elements inside a shadow tree can still see what nodes are distributed into their slots (on slotchange) even if that shadow tree is closed?

Yes, it's okay for Inner tree to see Outer tree, whether Inner tree is open or closed.
You might be interested in the definition of unclosed node. A lot of APIs (and concepts) are using this definition to define its behavior.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

@rniwa

I don't think you can create a flat tree mirror for closed shadow trees because all those slots that may have distributed contents would not be accessible from your code.

My code will have access to the distributed content because the elements being distributed into all the slots will be custom elements that I've defined within an private ES6 module scope. The flat-tree mirror can be constructed inside that private scope.

All I need to do is change my API so that it is a parent-to-child API instead of a child-to-parent API. This means that to construct the flat tree mirror I will have parent elements observing children instead of child elements observing parents. Get what I mean? I see the path now, so I'm gonna make that change soon with my elements.

@hayatoito

Yes, it's okay for Inner tree to see Outer tree, whether Inner tree is open or closed.

Perfect, thanks for the confirmation! :] (The description you linked to is like a completely foreign language to me. x] )

@trusktr trusktr changed the title How can a custom element detect when it is transcluded into a shadow tree? How can a custom element detect when it is transcluded into a shadow tree (slotchange)? Jun 6, 2016
@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

Specifically to my case, motor-node elementscan only be children of (or be distributed into)motor-sceneor othermotor-nodeelements, or an error is to be thrown. My code will need to detect when amotor-nodeis attached or distributed into something other than amotor-nodeormotor-scene`.

I see how to detect the following two cases and throw errors for them:

  1. motor-node can easily detect when child motor-node elements are attached with MutationObserver.
  2. motor-node can detect distributed child motor-node elements using slotchange.

But I am having trouble figuring out how to deal with this scenario:

  1. motor-node is appended to something other than motor-node. The child can easily observe when this happens. f.e. when motor-node is appended to a div there should be an error. But, the problem is: if the div has a ShadowDOM root (now or in the future) and the motor-node gets distributed into the div's inner tree into a slot element of an inner motor-node, then that is fine and there shouldn't be an error thrown.
  2. Furthermore, if a motor-node gets distributed into an inner tree and the slot-containing element is not a motor-node then there needs to be an error, and that case seems impossible to catch (with closed trees).

Basically, the following is fine -- a parent motor-node can easily detect the following:

  <motor-node> <!-- No error, this is the "parent" -->
       |
       |
    <slot>
       |
       |
  <motor-node> <!-- this is the "child", detected by "parent" via slotchange -->

But, the following is wrong, and I would like to throw a helpful error to the end user so they can learn from it:

     <div> <!-- Error, not a motor-node element -->
       |
       |
    <slot>
       |
       |
  <motor-node> <!-- This child is distributed erroneously. -->

Any ideas how to deal with those last two cases @rniwa and @hayatoito? Here's possibilities I think for each case:

  1. The motor-node can use setTimeout if not appended directly to another motor-node (for example is childNode of a div), then after the timeout throw a warning if a connection has not yet been made via slotting. But, it's impossible to tell if a shadow root will be added later, so I think only a warning, not an error, can be given. Or maybe I can keep it simple and just throw a warning up front if parent isn't motor-node and no ShadowDOM exists, but if there is already a shadow DOM (please correct if wrong) then distribution should happen synchronously (including the parent slotchange event) in which case I'll be able to deferr to a future tick in order to detect if a connection was made by a parent or not (assuming the connection happened synchronously before in the slotchange event). I've opened When is slotchange handler fired? #515 to ask about that specifically.
  2. For this case, maybe there needs to be a way for children to detect slotchange as well, but for distributed Nodes to simply not be able to access slot.parentNode when the ShadowDOM is closed. Or maybe a distributedCallback could be added to the spec, where the passed argument is the slot element, and slot.parentNode and slot.parentElementare null in that case (or similar), otherwise they can be used to look into the Shadow tree if it isopen`. I would really like to be able to inform the end user when such an error as in this case is made.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

In the case that a shadow root is created in the unknown future and while no shadow root exists so therefore a light-tree motor-node has no way to determine if it will ultimately be distributed into a proper location, then maybe I should just throw a warning about the fact that the motor-node is not currently attached to another motor-node and tell about the possibility that the motor-node may not get distributed into another motor-node.

I'd like for these errors/warnings to link to some documentation that I'll have online.

@treshugart
Copy link

Can you expand on your specific use case? It seems wrong that a child
should know anything about their ancestor tree as it breaks encapsulation.
Parents wrapping children should augment the behaviour of descendants
through events, but descendants should not couple their logic to the
ancestor tree.

On Tue, 7 Jun 2016, 07:50 Joseph Orbegoso Pea notifications@github.com
wrote:

@rniwa https://github.com/rniwa Specifically to my case, motor-node
elementscan only be children of (or be distributed into)motor-sceneor
othermotor-nodeelements, or an error is to be thrown. My code will need
to detect when amotor-nodeis attached or distributed into something other
than amotor-nodeormotor-scene`.

I see how to detect the following two cases and throw errors for them:

  1. motor-node can easily detect when child motor-node element is
    attached with MutationObserver
  2. motor-node can detect distributed motor-nodes with slotchange.

But I am having trouble figuring out how to deal with this scenario:

  1. motor-node is appended to something other than motor-node. The
    child can easily observe when this happens. f.e. when motor-node is
    appended to a div there should be an error. But, the problem is: if
    the div has a ShadowDOM root (now or in the future) and the motor-node
    gets distributed into the div's inner tree into a slot element of an
    inner motor-node, then that is fine and there shouldn't be an error
    thrown. Furthermore, if a motor-node gets distributed into an inner
    tree and the slot-containing element is not a motor-node then there
    needs to be an error, and that case seems impossible to catch (with closed
    trees).

Basically, this is fine, and a parent motor-node can easily detect this:

          <motor-node> <!-- this is the "parent" -->
               |
               |
            <slot>
               |
               |
          <motor-node> <!-- this is the "child", detected by "parent" via slotchange -->

But, the following is wrong, and I would like to throw a helpful error to
the end user so they can learn from it:

            <div> <!-- Error, not a motor-node element -->
               |
               |
            <slot>
               |
               |
          <motor-node>


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#504 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/AAIVbHzjTmvHC_19vvI26pDH5S6kKC5fks5qJJYdgaJpZM4IkCcX
.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

In my case, it's just that if a motor-node element gets attached or distributed into some other element that is not also a motor-node, then I'd like to throw an error or warning about this to the user. The problem I'm having is specifically when a motor-node is distributed into anything other than a motor-node I can't catch that case (because as you said, the child motor-node can not break encapsulation by reading the parent of the slot if the shadow tree is closed).

I'd really like to be able to inform the user about this case. AngularJS has nice errors that point to online documentation. I'd like to do this in that specific case, but I don't see how yet.

I'm thinking that the best solution (so far, in my mind) would be for Custom Elements to have a distributedCallback or slottedCallback (or similar) that is called when the custom element gets distributed into a slot (fired in the same tick as the slot parent's slotchange handlers). The single argument to the callback would be the slot element, and if the slot's shadow tree is closed then access to slot.parentNode/slot.parentElement would fail for encapsulation purposes. Additionally, it may be possibly arguable that the custom element who's distributedCallback is passed the slot cannot read the slot's children. But, I can probably imagine cases where a child would need info about it's sibling (even within a slot).

I technically now *don't need for a distributed element to be able to view a slot's parents in order to make my flat-tree mirror, but now I need a distributed motor-node element to be able to know it was distributed and to then be able to throw an error in a deferred tick (because in the previous tick a parent motor-node element will have established a connection with the motor-node child that is inside the slot.

Does that make it a little more clear the issue is I'm having?

@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

Maybe the improperly distributed motor-node can trigger an event, and if there's no response then it can be assumed that a parent motor-node does not exist on the other side of the slot and therefore throw the error that I wish to throw.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 6, 2016

I'd like to also note that in React this is super easy to do, thanks again to JavaScript scoping.

I think that if a custom element can know not only if it is connected, but if it is distributed (even if the shadow dom into which is it distributed into is closed) then all cases that I need to catch can be caught.

Maybe it makes sense for an element to know when it has been distributed, without necessarily needing access the inner tree's content (f.e. the inner tree is closed).

@trusktr
Copy link
Contributor Author

trusktr commented Jun 25, 2016

The solution to this problem is to have parent elements observe which children are attached to them or which children are attached to their slots when the parent is in a ShadowDOM tree. This is better than my current design where children observe parents in order to determine if child was attached to a correct parent, which doesn't work in closed Shadow trees because the child can't see the parent the slot where it is distributed into.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 25, 2016

Oops, nevermind. So parents observing children works in order to detect invalid children (and to detect invalidly distributed children when parent contains a slot and is in a shadow tree).

But, if a child is distributed into any element other than my custom elements (for example, on of my <motor-node> elements is distributed into a <div> or some other non-motor element), then there's no way for the distributed child to observe that it's parent is invalid if the Shadow tree is closed.

The only solutions I can think of right now are:

  1. Require shadow trees to be open. The children can easily observe parents.
  2. Add a new distributedCallback to the CustomElements API, and ensure that slotchange fires before a distributed child's distributedCallback is fired. This option would allow a parent to make a connection with the child in slotchange. Then, if no connection is made, a child would be able to detect that no connection was made inside of it's distributedCallback.

I've been thinking about this for weeks (in the back of my head while I work on other things) and can't find a solution.

@treshugart @hayatoito @rniwa @domenic Any ideas or thoughts?

To put my problem concisely, I just need the following <motor-node> element to detect that it was distributed into an element other than a <motor-node> or <motor-scene>, considering that the shadow tree is closed:

   <anything>
       |
       |
    <slot>
       |
       |
  <motor-node> <!-- This child is distributed into a <anything>, which is wrong, and I want to throw an error. -->

Where <anything> is any element besides <motor-node> or <motor-scene>. I want to throw an error in this case.

When the following happens, I do not want to throw an error:

  <motor-node>
       |
       |
    <slot>
       |
       |
  <motor-node> <!-- This child is distributed into a <motor-node>, which is fine, no error to be thrown. -->

@trusktr
Copy link
Contributor Author

trusktr commented Jun 25, 2016

I posted a new issue with the concept of distributedCallback. The reason for the idea is as a solution to this problem, but maybe there's alternatives which I would like to hear about if you know of any.

@treshugart
Copy link

@trusktr unfortunately I haven't had the use-case yet of requiring a child to know when it's distributed. In all cases I've come across, the child just may not be fully functioning due to parents not responding to events. I guess in that case, such a feature would allow me to warn the developer about it, but I'm not sure if having this feature outweighs the potential abuse it might get from devs breaking encapsulation. I can definitely see your point, though.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 28, 2016

@treshugart I can't see how to make it work with events. For example, how will the child know when to dispatch an event, as there's no way for the child to know it has been distributed? The only thing I can imagine is a poll-like method where the event is dispatched every so often, which is undesirable. Events seem like a way for an element to shout, but not receive a guaranteed response.

Being able to know when an element is attached (we already can with connectedCallback) and when an element is distributed (even if access to the slot is denied) would really help. For example, if a child element knows it has been distributed, then it can fire an event and if it receives no response then we can guarantee that a parent exists or not without a poll-like mechanism, and we'd be able to know this information at the soonest possible moment in time (I'm assuming events are synchronous?).

I'm wrestling with this because I really want to be able to create virtual scene graphs that mirror the flat tree. This makes sense from a rendering perspective, because that's what the browser does natively to render things, but in my case I want to have my own WebGL pipeline, so just like the browser I need to be able to know the flat tree structure to finally render what I want in WebGL, and I want to be able to throw meaningful warnings or error in the case that an HTMLElement is not distributed properly into a ShadowDOM tree.

My ComposedTreeProxy idea could probably be another way to solve this, whereby a child could read the proxy to determine if a certain parent exists (the proxy might be observable with MutationObserver in order to know when it's structure changes, or similar). But, this seems like a heavy-weight solution for my case of determining if a certain immediate flat-tree parent is present for a given child.

Any other ideas?

@treshugart
Copy link

@trusktr the connectedCallback() will be fired when the element is attached as light DOM. It shouldn't matter where it's slotted for your use case, just that it's been attached to a <motor-node>. In this case you can just check child.parentNode instanceof MotorNode.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 29, 2016

@treshugart That case is easy to detect, but the case I'm trying to detect is different: I need to ensure that an element is slotted into another element of a certain type. With a closed tree, I don't see any possible way for a child to know it was slotted into some unexpected element. For example, if <motor-node> is slotted into a <div>, then I'd like to do something in that case, but the slotted <motor-node> can't detect this scenario. The only way I see to detect it currently is for child to emit an event, and if the parent of the slot that contains child is of the expected type, then that parent can signal to the child (in the event handler) that child was slotted correctly, but the task of the child firing an event can not happen at the specific moment in time right after the child has been slotted because the child has no idea when it is slotted (My distributedCallback would help in this regard). If distributedCallback existed, then the child could fire an event in that callback, and if the child does not receive a response from a parent then the child can make the assumption that said child was not slotted into an expected type of element and we can act upon that condition in a guaranteed manner (throw an error or something else).

@treshugart
Copy link

If I understand you correctly this jsbin should work just fine. Just swap v0 for v1 API calls and Bob's your uncle. A child shouldn't need to know where it's slotted; in a composed tree, a child is always slotted into a <slot> or is a light DOM child of another node. In the case where it's composed parentNode is a <slot> then it will always report that its parent is the shadow host, thus the attachedCallback() / connectedCallback() should be sufficient.

@trusktr
Copy link
Contributor Author

trusktr commented Jul 1, 2016

Hey Trey, I really appreciate your effort to help me out. In my case, the situation is a little different. Here is a jsbin showing the problem: http://jsbin.com/dozacacuva/edit?html,js,output

In particular note that we are interested in the flat-tree composition being such that <motor-node> elements are only inside of <motor-scene> or <motor-node> elements.

@treshugart
Copy link

treshugart commented Jul 1, 2016

Ahh, ok. Interesting! I think I get it now.

In SD v1 you'll have access to an assignedSlot property on slotted nodes for open shadow trees. Would you be able to use motorNode.assignedSlot.parentNode in the case where you want to check the composed tree?

You can use our polyfill if you want to try it out.

EDIT

For closed shadow trees, I don't see how a distributedCallback would help because I assume that in a closed tree, the slotted node should not be able to know about the slot it was distributed to just like you can't access the slot via assignedSlot.

@trusktr
Copy link
Contributor Author

trusktr commented Jul 1, 2016

Ah, oops, I forgot to mention! In the jsbin I posted, the assumption was that the trees were closed, so motorNode.assignedSlot.parentNode wouldn't work in that case.

For closed shadow trees, I don't see how a distributedCallback would help because I assume that in a closed tree, the slotted node should not be able to know about the slot it was distributed to just like you can't access the slot via assignedSlot.

That's true, but with distributedCallback, the element wouldn't know about the closed-tree slot, because in that case the slot argument to distributedCallback would be null, but none-the-less the element would still be aware of the distribution action, would be aware that it was distributed, just that it can't access the slot or the inner tree it got composed into. So with distributedCallback, an element can still be aware of the fact that it got distributed, even without having access to any data (slot == null). It's like if someone took me and blindfolded me then placed me in the trunk of a car, I wouldn't be able to see anything, but I'd still know I was getting moved (distributed) to somewhere. If the tree is open, then slot is the reference to the slot element (they put me in the trunk without blindfold). 😆

@treshugart
Copy link

It's like if someone took me and blindfolded me then placed me in the trunk of a car, I wouldn't be able to see anything, but I'd still know I was getting moved (distributed) to somewhere. If the tree is open, then slot is the reference to the slot element (they put me in the trunk without blindfold).

Hah! Yeah, but would that be much different than using conenctedCallback() and assignedSlot even if it was null? When you're connected, you're distributed somewhere whether it's a <div> or a <motor-scene>. A <div> is essentially a custom element with a closed shadow root, even if it's not implemented that way at the browser level.

Another way of looking at this might be:

connectedCallback () {
  this.distributedCallback(this.assignedSlot);
}

@trusktr
Copy link
Contributor Author

trusktr commented Jul 2, 2016

That doesn't work because the connected events and distribution events don't necessarily happen at the same time.

When we write the following

    <div>
      <motor-scene id="scene">
        <motor-node id="node"></motor-node>
      </motor-scene>
    </div>

and it gets rendered, the motor-node's connectedCallback is fired. At that moment in time, assignedSlot is null.

Suppose we add a shadow root to the motor-scene, containing:

#shadow-root
  <motor-node id="inner-node">
    <slot>
    </slot>
  </motor-node>

Then we get:

  <div>
    <motor-scene id="scene">

      #shadow-root
        <motor-node id="inner-node">
          <slot>
            <!-- distributed motor-node -->
            <motor-node id="outer-node"></motor-node>
          </slot>
        </motor-node>

      <!-- original motor-node -->
      <motor-node id="outer-node"></motor-node>
    </motor-scene>
  </div>

At some point in time after adding the shadow root, motor-node's assignedSlot will be set to the above slot element, but motor-node's connectedCallback won't fire, so we can't rely on connectedCallback to know when the distribution happens.

With closed shadow trees, the value of assignedSlot remains null the whole time and we can't poll assignedSlot to discover when distribution has happened.

distributedCallback (or perhaps assignedCallback matching the v1 terms) could be an official mechanism that we could rely on in order to have such insight.

The main reason why I want such insight is that I want to make an API that's as easy to use as possible, and I don't just mean that the API is easy to use when documentation is followed, but that the API is easy to use because it will do a good job of teaching usage patterns to someone when they try the API by trial and error, and which will do a really good job of catching invalid use cases in applications where life and death could possibly even be at hand.

Something like assignedCallback (or some other official mechanism that makes it easy for an element to know it's been distributed) would make it easier for a library or framework author to write an HTML API that can detect more use cases (and react to them).

@trusktr
Copy link
Contributor Author

trusktr commented Jul 2, 2016

Is there any event associated with distribution? I know slotchange is good for the inner tree and owner of a slot, but for distributed nodes, maybe something like an assigned event could be nice.

@hayatoito
Copy link
Contributor

hayatoito commented Jul 11, 2016

I do not see any clear reason why we need distributedCallback.
The problem which distributedCallback solves looks non-essential to me. Rather, that sounds an opposite of interests. It is an anti-pattern, isn't it?

e.g.
Suppose <summary> and <details> elements.

  • <summary> element does not throw any exception nor emit any warnings even if it is used outside of <details> element.
  • <summary> element should not have such an initiative. Rather, it's <details> element's responsibility to coordinate with a <summary> element. <details> should take care of <summary>, but the opposite is not true.

@rniwa
Copy link
Collaborator

rniwa commented Jul 11, 2016

I think the use case outlined here is an interesting one. If I understand it correctly, this is really about introducing a new markup language on top of HTML to render 3D graphics like MathML does for mathematical questions and SVG does for vector images.

And I can see that to build something like that, one has to walk across composed/flat tree to see parent/child relationship as they're presented. Unfortunately, I don't think this is something we currently support to be implemented in the author code. We don't even support using shadow DOM in SVG/MathML context so the scope and usefulness of shadow DOM is quite limited in that respect.

Fundamentally, author code can't see the entire flat / composed tree across all shadow and slot boundaries in the presence of closed shadow trees. e.g. if motor-node is inside a shadow tree A, and its shadow host is assigned to another shadow tree B's slot whose parent is a motor-scene, then there is no way for motor-node or motor-sconce to access one another.

As things stand, you probably need to limit the use of your library to be entirely in a single tree (and not cross any shadow/slot boundaries). This is an interesting food for thought when we're considering imperative API and more fine grained distribution mechanism in general as well as Houdini.

@trusktr
Copy link
Contributor Author

trusktr commented Jul 16, 2016

@rniwa

this is really about introducing a new markup language on top of HTML to render 3D graphics like MathML does for mathematical questions and SVG does for vector images.

Yes, I suppose it is. Custom Elements make it possible to design markup language for anything. SVG and MathML elements (if they didn't already exist) would be the sort of API people could design on top of HTML using Custom Elements (as opposed to them being native elements). A great example of a custom renderer is Three.js built on WebGL. A-Frame is a great example: it takes Three.js and builds an HTML interface on top of it with Custom Elements, so that we can write Three.js-based scenes using markup. I'm aiming to do something similar (will be using WebGL, though possibly not Three.js), where my elements also make it possible to create a 3D scene with markup.

And I can see that to build something like that, one has to walk across composed/flat tree to see parent/child relationship as they're presented.

Not necessarily. I currently see how to use my elements in ShadowDOM as it currently stands (v0, all roots open). But I think even with closed trees, there is still a way to send events (or for parents to observe their children or their slots distributed nodes when those parents are in a shadow tree) in order to attach the virtual scene graph together.

My motor-node elements have a requirement: they must be children of each other in the flat tree so the scene is easy to reason about, and there won't be any sort of positioning based on an ancestor that is not a direct parent like there is with CSS position:absolute). Placing a motor-node in any element besides another motor-node or a motor-scene element won't work, and a connection will simply not be made because parents are observing direct children.

With closed trees this is fine because in a closed tree a parent can still see what is distributed into it's child slot, so a motor-node element can make a connection with motor-node elements distributed into slots that are children of that motor-node; crossing a ShadowDOM boundary therefore isn't necessary for building the scene graph, with one exception: shadow host elements need to know what are the immediate root-most elements of their shadow root.

I'm guessing in v1 that host elements can look at the elements inside their direct shadow root, right? And, if so, how is this possible? Does the code that accesses the shadow root have to run inside the methods defined for that custom element (f.e. inside of the element's connectedCallback)?

To put it more simply, without being able to know the whole flat tree, I believe I can still connect my scene graph together assuming the end developer follows the rules:

  • A motor-node element can only be child of
    • other motor-node elements.
    • motor-scene elements.
    • slot elements that are children of
      • motor-node elements.
      • motor-scene elements.
    • a shadow root who's host is a
      • motor-node element.
      • motor-scene element.

I believe all those cases can currently be detected (please verify about host element viewing shadow root children), which means I can use ShadowDOM with my elements. The only thing that I cannot detect with closed shadow trees is

  • A motor-node is child of
    • a slot who's parent is not a motor-node or a motor-scene.

I cannot detect that case because a distributed node cannot observe ancestors of the slot in which the node is distributed. This is just fine though, as far as functionality, because in that case I do not want my elements to make a scene graph connection. So, if a user of my library does that, then it will simply not render anything.

That is okay, and if they ask for help I can tell them why.

But! I'd like to throw an error in this case to inform the user of the situation so that they don't even have to ask for help if they are reading the helpful message in the console, but since it is impossible for my library to detect that scenario (without the above distributedCallback and slotchange events being fired first), then I cannot throw a helpful error message. Instead, my library will silently just not make a connection from a parent motor-node to the distributed motor-node, and that lonesome distributed motor-node will just sit there inertly, doing nothing. That is okay, but not as nice as if I could legitimately detect that case.

So, without distributedCallback, my library can still render scenes if the elements are used as in the list of scenarios above, and those ways of arranging motor-node elements in DOM will be the way that is publicly documented.

Unfortunately, I don't think this is something we currently support to be implemented in the author code. We don't even support using shadow DOM in SVG/MathML context so the scope and usefulness of shadow DOM is quite limited in that respect.

It should, and let me explain why. This new ShadowDOM stuff and Custom Elements should work with MathML and SVG too, and basically should just work anywhere where we write HTML DOM in the browser. The WebComponents spec allows an amazing modular component model of designing components (widgets, things to be rendered, UI controls, etc), making code much more re-uasble, easier to organize, and able to accept various type of children via out tree markup to render internally in the inner tree markup (markup --> DOM, markup translates to instantiated DOM).

So, with that said, why not have Web Components work anywhere, with SVG, MathML, and custom libraries like A-Frame or my own.

SVG can be a more powerful 2D graphical engine than HTML's 2D parts, and I see no reason (especially since event handling is involved) why we should not be able to componentize and modularize a user interface that may be made entirely of SVG elements. I can imagine making user interface components made entirely with SVG elements, and therefore I can imagine how useful WebComponents would be in making those UI components easily re-usable across projects.

So, I really think WebComponents (all of it, Custom Elements, ShadowDOM, etc) should apply anywhere where we will write HTML markup (or anywhere where we create elements with createElement(), without regard to the type of elements being used. It might make sense for certain elements not to have shadow roots, and it can be that element's definition that decides that, but we should definitely not void an entire set of elements (SVG) from taking advantage of it.

As things stand, you probably need to limit the use of your library to be entirely in a single tree (and not cross any shadow/slot boundaries). This is an interesting food for thought when we're considering imperative API and more fine grained distribution mechanism in general as well as Houdini.

With my library I will be making UI components, and I'd really like to take advantage of ShadowDOM for modularity and componentization. I strongly disagree with limiting my library by not using ShadowDOM.

Consider the following! An HTML developer discovers A-Frame, my library, or some other library that exposes custom elements. Then, that developer simply wants to take advantage of modularity and wants to make components with the custom elements by placing them into his/her own custom elements that take advantage of the benefits of ShadowDOM. One day, an author will inevitably try to place A-Frame's <a-entity>, my <motor-node>, or <whatever-it-may-be> into a shadow root and/or distribute nodes into one of those elements using slots, due to the mere fact that this new ShadowDOM API exists.

If we are to encourage the awesome patterns that ShadowDOM introduces, we should do so by encouraging people to use it everywhere for making components, not just sometimes, and with only elements X,Y,Z. That defeats the purpose of the feature. It should be a broadly generic feature that can work with any elements except for X,Y,Z elements that explicitly define so for good reason. Disabling them with SVG doesn't have a good reason (or at least I bet I can refute whatever the reason currently is).

There's a lot of info in my response, but TLDR:

  • distributedCallback is a small change that completes a small part of the big picture, and I think it can allow for useful behaviors like an element being able to detect when it has not connected with a specific parent in a shadow tree (without needing to reveal the shadow tree structure)
  • ShadowDOM should work with all elements as much as possible. It would be especially nice to use ShadowDOM with SVG and MathML. Those rendering engines can simply get the flat-tree from the HTML engine to render what they will finally render, so if the reason for not supporting SVG or MathML is technical, then refactoring should happen, but from the outer perspective of an HTML developer, there's no theoretical problem with writing SVG using ShadowDOM to distributes SVG elements into other places of an overall SVG DOM.

Here's an example, a "push pane layout" where the pane can be swiped into the view from the edge of the screen (logic omitted, but this demonstrates the HTML API that the push-pane-layout allows for):

class PushPaneLayout extends MotorHTMLNode { // motor-node <-> MotorHTMLNode
    constructor() {
        this.root = this.attachShadow({mode: 'closed'})
        this.root.innerHTML = `
            <motor-node>
                <slot name="header">
                </slot>
            </motor-node>
            <motor-node
                position="-100, 0,0" data-info="<-- Here we set the X position so the pane is hidden off the edge of the view."
                class="touch-enabled-push-pane">

                <slot name="pane">
                </slot>

            </motor-node>
            <motor-node>
                <slot name="body">
                </slot>
            </motor-node>
            <motor-node>
                <slot name="footer">
                </slot>
            </motor-node>
        `
    }
    // ...
}

customElements.define('push-pane-layout', PushPaneLayout)

Let's use the layout (end user doesn't have to know how layout logic works, just supplies content):

<push-pane-layout><!-- this is a motor-node -->
    <motor-node slot="header">
        <img src="logo.png" />
    </motor-node>
    <motor-node slot="pane">
        <ul>
            <li> menu item 1 </li>
            <li> menu item 2 </li>
            <li> menu item 3 </li>
        </ul>
    </motor-node>
    <motor-node slot="content">
        <!-- Some logic changes the content dynamicaly based on a URL route, etc... -->
    </motor-node>
    <motor-node slot="footer">
      Copyright (c) Some Company 2018
    </motor-node>
</push-pane-layout>

Now, suppose, an end app developer uses the push-pain-layout asa template for various pages of an app. Maybe depending on URL routes, the menu content might change:

class DynamicMenuLayout extends MotorHTMLNode {
    constructor() {
        this.root = this.attachShadow({mode: 'closed'})
        this.root.innerHTML = `
            <push-pane-layout>
                <motor-node slot="header">
                    <img src="logo.png" />
                </motor-node>
                <motor-node slot="pane">
                    <slot name="menu"> <!-- <<------------------------ -->
                    </slot>
                </motor-node>
                <motor-node slot="content">
                    <!-- Some other logic changes the content dynamicaly based on a URL route, etc... -->
                </motor-node>
                <motor-node slot="footer">
                    Copyright (c) Some Company 2018
                </motor-node>
            </push-pane-layout>
        `
    }
    // ...
}

customElements.define('dynamic-menu-layout', DynamicMenuLayout)

Now using that dynamic-menu-layout in HTML would be used like this (imagine this could be the markup in the shadow root of yet another Custom Element):

<dynamic-menu-layout>
    <motor-node slot="menu">
        <ul>
            <!-- Some logic changes this menu content. -->
        </ul>
    </motor-node>
</dynamic-menu-layout>

As we can see here, being able to use ShadowDOM is very useful! We should definitely make ShadowDOM work everywhere possible so that developers who adopt Web technology as their front-end engine can use a single awesome pattern for modularity (Custom Elements + Shadow DOM).

As it currently stands (based on what you said), we don't have the option of using ShadowDOM with SVG, and that is definitely a bummer to someone who decides that their UI should be entirely SVG (which is totally possible, maybe they are an Adobe Illustrator expert who just picked up HTML and exported UI designs to HTML markup, then read an awesome article about how to componentize HTML components only to find it completely fails with his nothing-but-SVG UI).

@trusktr
Copy link
Contributor Author

trusktr commented Jul 16, 2016

Note, in my example, I used excess motor-node elements, and I probably won't end up structuring my actual components exactly like that, but it paints the picture and shows that it should be possible to code in that manner with ShadowDOM (regardless of the type of HTML elements being used).

People who adopt libraries like React, Riot.js, etc, they don't have any limitations on what elements they can use. They can render any elements including SVG and MathML with those libraries, and so the web should aim to allow that as well with ShadowDOM. ShadowDOM + Custom Elements + ETC (Web Components) are a design pattern that we should be able to use when defining any HTML structures.

@trusktr
Copy link
Contributor Author

trusktr commented Nov 25, 2016

@treshugart You mentioned,

there's a slotchange event being implemented in v1 that is a non-bubbling event fired on slot elements when the slotting algorithm runs. More discussion in #288. Would this solve your issue? I noticed the web component API you're using is the one Blink had prior to v1. AFAIK there's no way to detect this in "v0" without using a Mutation Observer on your elements.

Mind explaining how to observe this on <content> elements with MutationObserver? As far as I know, there is only the childList that gets close, but does that also give us distributedNodes that aren't actually children of the content element?

@trusktr
Copy link
Contributor Author

trusktr commented Nov 29, 2016

@treshugart I tried using MutationObserver on <content> elements, but no luck. Can you provide an example?

@trusktr
Copy link
Contributor Author

trusktr commented Dec 1, 2016

@treshugart Nevermind, I think I got it: for a given <content> element that we wish to observe for distribution changes, we can traverse up from it to find the shadow root host, then use MutationObserver on the host to detect changes in children, and whenever changes happen we just call .getDistributedNodes() on the <content> element to find the new distribution and compare against the previous result to determine if there was a change in distribution.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 1, 2016

It might still be nice for a Custom Element to detect distribution (without leaking structure of a shadow root) because then with this knowledge it can trigger an event if it needs to. This is not possible with slotchange because there's no way for a slot to determine which event a distributed node should fire.

cc @rniwa @hayatoito ^, What do you think?

After thinking about this for a while, and after successfully making my library compatible with ShadowDOM V1 including closed shadow roots, I still think the a solution to the need described in this issue would be invaluable because then I would be able to detect certain conditions that I can not otherwise detect without crazy hacks, in order to give the end user of my library helpful console messages like

Warning: Your motor-node#someID element does not work when composed as a child of such-an-such element. It must be distributed to a slot which is child to a motor-scene or motor-node element. See http://... for more details.

With slotchange and assignedNodes, I can only detect proper use cases where a shadow root is the root of one of my library's custom elements and when a slot is child of one of my library's custom elements and when one of my library's elements are connected or distributed directly to another element of my library. Detecting the other cases would be too hacky and expensive so far as I can imagine (involving hijacking attachShadow and traversing all shadow roots).

@treshugart
Copy link

treshugart commented Dec 2, 2016 via email

@trusktr
Copy link
Contributor Author

trusktr commented Dec 2, 2016

@treshugart

only check and warn in dev

True, good idea.

I just realized that I would also need to use a MutationObserver with the attributes: true option on all the children of the shadow host, because changes in attributes can lead to changes in selector matches for the shadow root <content> element.

Basically, it seems like implementing the equivalent of slotchange for v0 <content> elements is really expensive.

@hayatoito
Copy link
Contributor

hayatoito commented Dec 6, 2016

Let me close this since it looks this topic is discussing how to support v0. That is not what we will support, in terms of the spec.

@trusktr
Copy link
Contributor Author

trusktr commented Sep 10, 2021

I opened a more concise version of this question, but pertaining to v1, over in #941.

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

No branches or pull requests

4 participants