_(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)_
I'm not sure what a good title for this issue is.
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?
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 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.
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.
@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.
Regarding the support of slotchange events in Blink, I am now implementing it at https://codereview.chromium.org/1995203002
@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)?
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.
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
@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.
Yeah, no way for a closed mode. We have to honor the intention of a component author who chooses "closed" intentionally.
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.
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.
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.
@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] )
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:
motor-node can easily detect when child motor-node elements are attached with MutationObserver.motor-node can detect distributed child motor-node elements using slotchange.But I am having trouble figuring out how to deal with this scenario:
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.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:
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 https://github.com/w3c/webcomponents/issues/515 to ask about that specifically.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.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.
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 [email protected]
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:
- motor-node can easily detect when child motor-node element is
attached with MutationObserver- motor-node can detect distributed motor-nodes with slotchange.
But I am having trouble figuring out how to deal with this scenario:
- 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
https://github.com/w3c/webcomponents/issues/504#issuecomment-224099588,
or mute the thread
https://github.com/notifications/unsubscribe/AAIVbHzjTmvHC_19vvI26pDH5S6kKC5fks5qJJYdgaJpZM4IkCcX
.
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?
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.
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).
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.
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:
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. -->
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.
@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.
@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?
@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.
@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).
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.
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.
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.
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). 😆
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);
}
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).
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.
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.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.
@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:
motor-node element can only be child ofmotor-node elements.motor-scene elements.slot elements that are children ofmotor-node elements.motor-scene elements.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
motor-node is child ofslot 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)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).
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.
@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?
@treshugart I tried using MutationObserver on <content> elements, but no luck. Can you provide an example?
@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.
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).
To get around the expensive checks, you could make it so it can only check
and warn in dev so that the expensive checks aren't run in prod code.
On Fri., 2 Dec. 2016, 10:59 Joseph Orbegoso Pea, notifications@github.com
wrote:
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.After thinking about this for a while, and after successfully making my
library compatible with ShadowDOM V1 including closed shadow roots
https://github.com/trusktr/infamous/issues/40, 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 distributed to such-an-such element. It must be distributed
to a motor-scene or motor-node element.`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).—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/w3c/webcomponents/issues/504#issuecomment-264333536,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAIVbO87Q7bJfosEptFGeRY5aPEsGaO7ks5rD19NgaJpZM4IkCcX
.
@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.
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.
Most helpful comment
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-nodeis inside a shadow tree A, and its shadow host is assigned to another shadow tree B's slot whose parent is amotor-scene, then there is no way formotor-nodeormotor-sconceto 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.