From a blink bug, an author reported that connectedCallback is called too early in Blink.
<script>customElements.define('x-x', ...);
<script>customElements.define('y-y', ...);
<x-x>
<div></div>
<y-y></y-y>
</x-x>
When current Blink impl runs this code, this.children.length is 1 in connectedCallback for x-x. This looks like matching to the spec to me, but I appreciate discussion here to confirm if my understanding is correct, and also to confirm if this is what we should expect.
My reading goes this way:
x-x as Any other start tag, it insert an HTML element.connectedCallback.div. In its create an element for a token, _definition_ is null, so _will execute script_ is false. This div is then inserted.y-y. In its create an element for a token, _definition_ is non-null, so _will execute script_ is true.connectedCallback runs here.Am I reading the spec correctly? If so, is this what we should expect?
@domenic @dominiccooney @rniwa
In enqueue a custom element callback reaction, it adds to the backup element queue.
I think you are correct that the spec says this, but this also seems wrong. The backup element queue should be avoidable when we are doing parsing; it should only be used for things like editing. We should be able to just use the element queue created for that element.
That would make connectedCallback be called with zero children, right? That sounds much better.
I am not sure what the best spec fix is for this---it is getting late over here---but maybe it would suffice to just push an element queue in "insert a foreign element", then pop it and run reactions? Better suggestions appreciated.
Yeah, 0 sounds much better than 1, and "insert a foreign element" looks the right place.
I also understand web developers might expect 2 instead of 0, but that's harder, wondering whether the benefit is worth the complexity or not.
No. We absolutely want zero children in as many places as possible. The whole point is that you should not depend on children, since you cannot depend on them with createElement() either. Web developers should create robust custom element implementations, not half-baked ones.
In constructor, yes, but in connectedCallback, authors can add children before adding the element to a document, no?
Ah yeah they could. But that should not be required by the custom element.
I'am the author of the blink bug,
Ah yeah they could. But that should not be required by the custom element.
Could you explain why ? connectedCallback() is called when the node is inserted in a DOM, so i think it should be aware of its own children ?
If not, how/when a CE can work with its children ?
Also it's very bizare that connectedCallback() doesn't have the same capability when the node is created from the parser or upgraded.
When the node is created and inserted, it doesn't have any children.
So... how do you work with custom elements that have expectations about their children?
Mutation observers.
So connectedCallback is called with 0 children in the case of upgrades, but if I am to do:
var myEl = document.createElement('my-element');
myEl.appendChild(document.createElement('span'));
var frag = document.createDocumentFragment();
frag.appendChild(myEl);
document.body.appendChild(frag);
Then "my-element"'s connectedCallback is called when the fragment is inserted into the body and it will have 1 children in this case.
So a custom element with expectations on children will need to:
No concerns here, just trying to work it out in my head.
If you set up the mutation observer at construction time, that first appendChild() can trigger do stuff too, but if you only want to observe while connected something like that could work.
I meant it can cause do stuff to be scheduled. Mutation observers run end-of-task so not immediately.
An alternative is to add childrenChangedCallback: https://github.com/w3c/webcomponents/issues/550
Mutation observers.
For now in chrome canary ( i don't know if it matches the spec or not), if i try to get all children with an observer even in constructor(), the observer is fired 2 times as you can see in this jsfiddle.
So if i just want to have a list of initial children, i don't know when i have to disconnect the observer and do the work?
An alternative is to add childrenChangedCallback: #550
Why not if all initial children are returned in one call.
The whole idea of initial children is flawed. Children are dynamic.
Indeed. In general the multi-tag designs of HTML (e.g. <select> + <option>, <ul> + <li>, <picture> + <source> + <img>, etc.) are very fragile; the amount of spec machinery involved in making them work correctly is extensive and in some cases still buggy in the current spec. (We don't even have a correct design for <li> yet, this many years later: https://github.com/whatwg/html/issues/1617.) Web devs should be very cautious when trying to set up such designs for their custom elements; you need to be aware of how children change dynamically, and not just use the children as some kind of setup data.
It's worth noting that in the past we've talked about adding a new callback for this sort of thing, I think someone called it closeTagReachedCallback or something. That would be useful for emulating <script> behavior, I think.
FWIW, I would object to adding something like closeTagReachedCallback because they're specific to HTML parser behavior. I'd much rather add something like childrenChangedCallback, which can be used either during DOM mutations or during parsers.
Thanks domenic for this complete answer.
I thought indeed to this type of case, an element that sort its children or compute a layout.
For example to create an element that compute a sexy layout, its important to access to an initial state of children to avoid re-compute the layout many times(each time a new CE children is parsed) for just the first render...
So i understand we should avoid to use the children as some kind of setup data, but what is the main technical reason to not to do that ?
For example to create an element that compute a sexy layout, its important to access to an initial state of children to avoid re-compute the layout many times(each time a new CE children is parsed) for just the first render...
What I'd suggest doing is just resorting or re-rendering your children on every requestAnimationFrame. That way you will not only get the post-parsing batching behavior you desire, but you will also synchronize your work with when it's displayed to users.
So i understand we should avoid to use the children as some kind of setup data, but what is the main technical reason to not to do that ?
I thought I explained why that was above, talking about how they are dynamic and can change any time, and this leads to complicated behavior.
What I'd suggest doing is just resorting or re-rendering your children on every requestAnimationFrame. That way you will not only get the post-parsing batching behavior you desire, but you will also synchronize your work with when it's displayed to users.
Ok, that's a good idea.
Even it's seems a - little - hacky for a brand new API.
I thought I explained why that was above, talking about how they are dynamic and can change any time, and this leads to complicated behavior.
Ok, thanks, it's bizarre but i trust you :)
Ok, that's a good idea. Even it's seems a - little - hacky for a brand new API.
Nah. When do you think browsers update rendering for their elements? (The answer is: every animation frame callback.)
Only if there is something to update though.
Sorry to come back but i was checking the SkateJS library and i saw that in the todolist example on the homepage:
...
attached(elem) {
// Setup the initial list of items from the current children.
elem.items = elem.children;
},
...
And attached() seems to rely on connectedCallback().
So i think it show the need to really clarify the situation - or - provide a real solution even requestAnimationFrame() could do the trick.
Yeah, this might be worth someone doing a blog post on or similar.
A library could provide that hook for you, or conversely a small function could give you the same behavior:
connectedCallback() {
onChildren(this, children => {
});
}
Fwiw you can probably use the slotchange event on slot elements for most
things. The skatejs-named-slots polyfill exposes that and makes native v0
behave like v1.
On Thu, 25 Aug 2016, 08:58 Matthew Phillips [email protected]
wrote:
A library could provide that hook for you, or conversely a small function
could give you the same behavior:connectedCallback() {
onChildren(this, children => {});
}—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/w3c/webcomponents/issues/551#issuecomment-242234361,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAIVbAr4qQnVroGn_kDRefmMkbIcRdfuks5qjMyTgaJpZM4Jqj2v
.
Not being able to see children in elements at connectedCallback() seems like a pretty big hassle when dealing with wrappers that are needed because of lack of support for customized built-in elements.
For example, where before we could write an extension to customize template behavior:
<template is="fancy-template">fancy stuff</template>
Now we have to write a wrapper:
<fancy-template><template>fancy stuff</template></fancy-template>
And in this wrapper we need to either create a MutationObserver, or wait a microtask, etc. (in addition to any MutationObserve that might have existed on the template's contents). Being able to see children in connectedCallback() makes these wrappers that are now required much simpler to write, and still work with createElement().
It sounds like what you're asking for is not connectedCallback, but a separate feature (like childrenChangedCallback or finishedParsingEndTagCallback). connectedCallback simply fires when the element is inserted into the DOM, and at that time, there are no children.
connectedCallback simply fires when the element is inserted into the DOM, and at that time, there are no children.
<my-el>
<p>child 0</p>
<p>child 1</p>
</my-el>
<script>
customElements.define('my-el', class extends HTMLElement {
connectedCallback() {
console.log(this.children);
}
});
</script>
If I understand correctly the current status, it's hard to explain it, with or without a blog post, that there are no children in above situation.
Right, that is an upgrade, and happens way way after the element has been inserted into the DOM.
It's these kind of inconsistencies between upgrades and non-upgrades that had people arguing for removing upgrades from the spec entirely. We did not go that route, but perhaps that perspective can be helpful in appreciating that they are rather different things. I agree it is hard to explain, but I think it's still worth keeping upgrades.
(Many other things are different between upgrades and non-upgrades, e.g. in the _constructor_ there are children during upgrades.)
e.g. in the _constructor_ there are children during upgrades.
which is why I am personally promoting a different approach such:
class HTMLCustomElement extends HTMLElement {
constructor() {
const self = super();
self.init();
return self;
}
init() {}
}
// so that everyone can extend it via ...
class MyEl extends HTMLCustomElement {
init() {
// any logic previously used as
// createdCallback();
}
}
Afaik that logic should have children and delegate to connectedCallback / disconnectedCallback the only addEventListener / removeEventListener or direct self node related logic.
Yet this means developers expectations might need a fix for standards they use, which is usually undesired.
If I might, and without sarcasm meant, the more I think about this upgrade part of the specs, the more I think it feels like "_quantum physics_" where it's not clear to consumers (users, developers) what a custom element is, or what it'll become.
The DOM so far never worked this way, I'm not sure an intermediate state is a good addiction to these specs.
Maybe it's easier for implementation purpose so that libraries on top are needed.
Just my 2 cents.
You can't rely on children in the constructor, that will only be the case for upgrades. Better to have your framework/library implement a hook for when children are ready. A childrenChangedCallback would be nice too, maybe we should take the discussion over there. https://github.com/w3c/webcomponents/issues/550
I apologize for continuing a thread that's closed, but I'm worried I might be confused -- shouldn't there be 2 children (instead of _no_ children) in the connectedCallback in @WebReflection's first example (because it's an upgrade, where the children have already been appended to the element)?
In an upgrade:
HTMLUnknownElement with children)vs. when parsing:
parentNode, etc.Do I have that right?
_In this case, I don't think @WebReflection's second example would help anything or guarantee children are present, right?_
As an everyday web developer, this generally makes sense and seems fairly straightforward to _me_. When parsing, the start tag is the thing that represents the element (not the end tag) and is what triggers construction, attributes, and insertion (in that order), whether an element is custom or not. The end tag is merely a marker that tells the parser where to do the insertion (that is, it's a _parser instruction_ and not really anything to do with the DOM).
Upgrades are the special magic that allows me to attach my behavior to an element that already exists and has been fully set up. (And thank you @domenic and everyone who fought to keep upgrades in the spec; they are incredibly useful to have even when they complicate things.)
I will echo that my naive first reaction is to ask for a finishedParsingEndTagCallback or something, but I ultimately think @annevk's comment that "the whole idea of initial children is flawed" is probably right. While I thirst for a way to unifying parsing and upgrading, having a way to get the "initial children" while parsing still doesn't unify them with the live reality of an element that can have children appended to it later. It's probably better to unify all three cases with ideas like childListChanged callbacks.
I don't think @WebReflection's second example would help anything or guarantee children are present, right?
I think I forgot to delay the init invocation so that it should be requestAnimationFrame(this.init.bind(this)) in the constructor.
Although it's easy to solve on library side, I don't think we need an extra method for that.
If knowing about children is mandatory for the component, a MutationObserver added in the connectedCallback should be enough, right?
As a developer i expect, that all dom structure parsed and accessible in connectedCallback.
CEReaction for element must starts only after parent element finished his connectedCallback event. Because parent can fully change his content.
connectedCallback must behave as connectedAndParsedAllChildrenCallback.
If component has child nodes but they inaccessible, connectedCallback is absolutelly useless for developers without children content.
Now all implementations connectedCallback behave as connectedAndParsedAllChildrenCallback. I dont't understand why new specification modify it.
How I minimize flash of non-upgraded children:
export default class TimeagoElement extends HTMLElement {
connectedCallback() {
if (this.querySelector('time')) {
this.init()
} else {
window.requestAnimationFrame(() => {
this.init()
})
}
}
...
}
As Firefox (Gecko/Quantum) seems to have implemented a different behaviour (children being available in the connectedCallback outright), and as the polyfilled browsers (document-register-element) seem to also grant access to a custom element's children in the connectedCallback, would this be the way to implement it cross-browser with minimal performance impact?
class MyElement extends HTMLElement {
connectedCallback() {
this.init();
}
init() {
if (this.children.length) {
this.childrenAvailableCallback()
} else {
setTimeout(this.childrenAvailableCallback.bind(this), 0)
}
}
childrenAvailableCallback() {
/* access to child elements available here */
}
}
@franktopel watch out attributeChangedCallback might trigger before that and with available children too.
The upgrading mechanism of Custom Elements is easily a footgun for expectations.
If you define the Custom Element before it's found in the body or after, you have already two different behaviors and different possibility to access their content.
In the former case, it'll break until the whole element has been parsed and all its children known too, in the latter case it'll work without problems right away.
Then you have the procedural way to create a custom element with a class ... and that might land on the DOM very late (or even never) so that any recursive interval/animation frame might leak forever if the node gets trashed before it's appended.
Using MutationObserver also might not work because if the custom element is known and it's defined later, no mutation will happen once upgraded.
Example:
customElements.define(
'my-early-definition',
class extends HTMLElement {
constructor() {
console.log('early', super().children.length);
new MutationObserver((records, observer) => {
observer.disconnect(this);
console.log('early', this.children.length);
}).observe(this, {childList: true});
}
}
);
document.body.innerHTML = `<div>
<my-early-definition>
<p>early</p>
</my-early-definition>
<my-lazy-definition>
<p>lazy</p>
</my-lazy-definition>
</div>`.replace(/\n\s*/g, '');
Copy and paste above code in a bank page. Read _early 0_ and _early 1_.
Now copy and paste the following:
customElements.define(
'my-lazy-definition',
class extends HTMLElement {
constructor() {
console.log('lazy', super().children.length);
new MutationObserver((records, observer) => {
observer.disconnect(this);
console.log('lazy', this.children.length);
}).observe(this, {childList: true});
}
}
);
Read _lazy 1_ and that's it.
Play around putting connectedCallback and attributeChangedCallback in the mix and see what happens with observed attributes already defined in the element.
Tl;DR it's complicated
P.S. with empty custom elements that might never be a way to understand if these have been initialized or not, 'cause checking childNodes.length won't be enough.
P.S.2 .. the early 0 happens only in Safari, not Chrome.
What a mess. I can't wait to see CustomElements v2 fix this mess and give developers a reliable lifecycle api that meets developer needs and expectations. Tbh I still don't get what Chrome is achieving with the current implementation behaviour, even after two days of messing around with this topic. Why does Gecko/Quantum allow reliable child element access in the connectedCallback, where Blink doesn't? How does the current document-register-element polyfill work in this regard? Where is this going?
How does the current document-register-element polyfill work in this regard?
with anything that makes sense, without granting any specific behavior since no browser is behaving the same of others.
The sad story short is that Custom Elements are a mess to setup unless you use shadow DOM which works in the constructor too, but shadow DOM cannot be reliably poly filled and whatever poly around weights already too much and is full of hidden caveats.
... but somebody still wonders how come WebComponents didn't take off as expected 🤷♂️
If I am not mistaken, then including the web components with type="module" fixes the missing children in the connectedCallback in Chrome. ?!?!?? totallyconfusednow
Does type="module" imply a similar parsing behaviour/timing as defer?
First off, it's literally impossible to have children or attributes before an element is constructed because we can't add children to an element yet to exist. Just think about the way a JS code would construct an element:
const someElement = document.createElement('some-element');
someElement.setAttribute('title', 'This is some element!');
someElement.appendChild(document.createElement('other-element'));
In the case of upgrading, however, the element already exists in the tree, and we're iterating over them to instantiate. In particular, if a custom element definition comes in a deferred script or an async script, then the custom elements that already exist in the document needs to be upgraded.
We could have gone our way to delete attributes & detach from children, but that would have caused serious performance issues in addition to flush of content.
So what approach is supposed to be used with loads of components in a page of which many rely on their children to setup? I mean, transforming their content which comes as plain HTML from the serverside to something different from my perspective is one of the main use cases for autonomous custom elements, as well as for customized built-ins which usually serve as containers for child elements.
What would be the downsides of the approach suggested in https://github.com/w3c/webcomponents/issues/551#issuecomment-429242035 (regarding components that rely on their children to setup)?
@franktopel : The problem then is that the element won't get connectedCallback until all children are parsed. For example, if the entire document was a single custom element, that custom element would never receive connectedCallback until the entire document is fetched & parsed even though the element is really in the document. That would be bad.
Alternatively, we could add something like finishedParsingChildrenCallback but then again, any JS could insert more child nodes after the parse had finished inserting children.
In general, the recommended approach is to either use MutationObserver to observe children being added or removed, or to have a child element notify the parent as it gets inserted. That's the only way a custom element would be able to behave correctly when JS, HTML parser, etc... inserts more children or remove some.
FWIW a lot of this was discussed upthread, with some good discussion worth (re-)reading. I am particularly fond of my response from two years ago: https://github.com/w3c/webcomponents/issues/551#issuecomment-241840803 :)
I did read and try to understand the thread, and some others, too. At the end of the day I'm just a developer who has to take care that the components I develop work reliably for the customer; and at this point in time, I feel unable to achieve that.
One of the concrete use-cases we have (just an example) is a <data-table> component that holds a regular HTML <table> element which needs to be datatableized with a very complex configuration. On top of that, we have the situation that we get asynchronous updates for that table in the form of, again, a <data-table> that has a regular child HTML table which we need to plug in to the page using morphdom. And this is where it fails: The custom element is already registered and defined, and by replacing the current <data-table> using morphdom the connectedCallback is called again, and doesn't find the slotted table inside.
Alternatively, we could add something like finishedParsingChildrenCallback
I think nobody needs that (meaning it doesn't semantically even scale), but createdCallback from V0 would be already great, assuming it triggers as soon as the live node has been parsed so that if the CE is defined upfront, it won't trigger until the end of the node is reached, and if the CS is defined after, it triggers as soon as the browser can access its content.
Basically your solution to put an element at the end of a Custom Element to know if its ready should be backed in the Custom Element API itself, not a per-developer responsibility, since that's the moment any custom element would like to setup.
Those created procedurally via JS can trigger the same thing via tick so that adding nodes on the fly synchronously would be still possible and the component can initialize itself properly right after.
If createdCallback from V0 is a bad name due history, let it be contentParsedCallback or even readyCallback so that at least the standard would provide a universal way to setup CEs without needing mandatory ShadowDOM to be consistent (yet with same problem if there was content inside the node or not during its construction/upgrade).
That sounds precisely like the kind of a use case MutationObserver would address.
Also, it's wrong to assume that child nodes would be inserted once and never change. Scripts can totally remove & add more child nodes later on so you'd have to have MutationObserver to observe those changes anyway.
That sounds precisely like the kind of a use case MutationObserver would address.
As already explained, MutationObserver doesn't trigger anything if the element is already live on the DOM and the custom element is defined after. There are example to test this.
https://github.com/w3c/webcomponents/issues/551#issuecomment-429262811
Also, it's wrong to assume that child nodes would be inserted once and never change.
Nobody assumes that, we need a way to setup once the custom element. The constructor is not a good place to setup a custom element if it doesn't use shadow dom and would like to initialize or parse/understand/query/use its content.
Scripts can totally remove & add more child nodes later on so you'd have to have MutationObserver to observe those changes anyway.
You keep ignoring the issue: how to setup a custom element.
We can talk forever DOM can change, we all know this, it's a useless discussion.
Nobody knows how to setup a custom element though, in a way that works with definitions already known, loaded on demand, or procedural.
It is a real problem with v1: but the compromise which changed the lovely v0 was essentially an fu to components, an effort to make CE non viable. The solution is actually Dom[0], way easier in es6, but are “naughty”. Require a monkey patch.
If child behavior depends on parent type, put a Shared function or a getter prop on HTML.prototype using Obj.defineProperty; make the getter a state machine/switch/proxy dependent on this.nodeNamr and/or this.parentElement.nodeName.
The getter on the HtmlElement.prototype will register parentsvand children immediately. There is no connected bullshit.
Here is a verbose declarative approach with animation, resize. And most of the features of flex without any css.
Nobody knows how to setup a custom element though, in a way that works with definitions already known, loaded on demand, or procedural.
I don't understand this. You just need to iterate over child nodes inside the constructor if there are any, schedule a MutationObserver on child node change on this and then re-iterate whenever child nodes are inserted or removed. Simple as that. The element needs to remain functional throughout this process after the constructor had finished running.
@rniwa So if that is as simple as that, could you be so kind and post a bullet-proof boilerplate for a custom element setup? Something that just makes the component work the way a developer would naturally expect it to? Or just link me to the page that has this explained in-depth so a regular developer can understand and apply it?
More than 2 years ago, @WebReflection stated the following and I can see myself sign that:
If I might, and without sarcasm meant, the more I think about this upgrade part of the specs, the more I think it feels like "quantum physics" where it's not clear to consumers (users, developers) what a custom element is, or what it'll become. https://github.com/w3c/webcomponents/issues/551#issuecomment-242571054
I admit I haven't used MutationObserver up until now, but it feels odd that I have to, given a mature specification like web components v1, which many people have worked on for like 4 or 5 years. Doesn't this even prove there's a missing lifecycle hook?
@jfrazzano Who would ever be interested in making CEs non-viable?
You just need to iterate over child nodes inside the constructor ... Simple as that
Simple, right? That assumes you know inside the constructor if the Custom Elements would expect nodes to setup itself, or not.
An empty custom element is a perfectly valid use case that fails your simple approach.
A Custom Element that might be forever empty or optionally have nodes to dictate its status is another perfect common use case, i.e. my-select that would see its shape only after known its content has one opt more my-option or not, before setting up its ShadowDOM or its final shape.
How do you know when it's the time for that component to initialize itself once if not by trusting some parent has a MutationObserver to take indirectly care of that?
This is impractical, and what is missing is a way to know, from the component, the component body has been fully parsed, which should happen once for a component lifecycle, and never more than once.
Just like a constructor, with all possible setup available, including a dirty innerHTML, which instead throws arbitrary errors if the custom element is being upgraded but not fully known (childNodes.length = 0).
As simple as that.
P.S. @rniwa even @cramforce (Google AMP Team) had this issue forever about this ( discussed also in here https://twitter.com/cramforce/status/975310752666984448 ) and he suggested me to check for nextSibling and then again, a node could be the only child so that knowing there's no nextSibling leads to false positives.
As Malte said, we need the implementation to give developers an API to know when it's safe/OK/fine to setup a custom element (once, not per each dom mutation inside it).
edit also the browser knows this, because indeed it arbitrary throws errors if it's too early, but it doesn't expose when is not.
@rniwa I can't stop thinking about this
Alternatively, we could add something like
finishedParsingChildrenCallback
Now, I don't care about finishedParsingChildrenCallback but I'd love to have a parsedCallback instead.
Yes, that might never happen if a Custom Element is the entire body of a huge page full of intermediate flushes, but that's a very confined problem, not the most common use case.
Basically, flushing this:
<body>
<div>
<my-early-definition <?php
flush();
usleep(300000);
?>
key="value">
<span>ear</span>
<?php
flush();
usleep(300000);
?>
<span>ly</span>
</my-early-definition>
<my-lazy-definition key="value">
<p>lazy</p>
</my-lazy-definition>
</div>
</body>
It doesn't matter if there is a Custom Element or a MutationObserver, the connected will never happen until the opening tag is finished (consistently in both Chrome and Safari).
That means that when attributeChangedCallback or a mutation record with its addedNodes is triggered, the beginning of the element is known.
That flush in the middle though, will always trick the browser to early trigger either a connectedCallback or an observed mutation within added nodes.
However, what's basically impossible to know in user land but absolutely known behind the scene, is when the closing tag is either enforced or found, so that nodes found after would either be children, sibling, or part of the parent.
That is what I'd love to have in custom elements so that it is possible to understand when the element is fully known or not.
new MyEl would trigger parsedCallback instantly after the constructorparsedCallback will trigger only once the whole node has been flagged as _parsed_ or _known_ or with a closed tag. Everything else remains the same but at least we have an entry point to setup / flag the component state (or show spinners via CSS and add a class to drop it later on ... and similar stuff)parsedCallback would trigger possibly before any of the attributeChanged/connectedCallback friends but yet, if that's complicated, it's important that triggers ASAP, no matter what.The parsedCallback would provide a primitive mechanism that would make any real-world Custom Element user happy because it exposes an extremely important information that is vital to understand, handle, or setup, reliably in both client and server rendered code Custom Elements.
Thanks for considering that.
As a data point, AMP's custom element base class has a custom callback called "buildCallback" named to signal the point when custom elements should be safe to "build" there child structure.
This is currently implemented as:
connectedCallback firednextSiblingFor the vast majority of elements this is strictly a better time to do initialization than connectedCallback. Currently connectedCallback may be called with and without children present. That kind of racy behavior leads to bugs all over the place. Requiring use of MutationObserver for cases where actual mutations are unexpected is a really bad programming model and prone to buggy code.
Of course, some custom elements must be initialized before children are parsed. This is primarily the case for container elements that may have large amounts of child nodes and waiting for all of them to parse would break streaming rendering. This needs to continue to be supported but that isn't a good argument that there shouldn't be a shortcut for the the more common use case.
I _still_ think that a dedicated childrenChangedCallback or similar is needed to fix this very rough edge of the APIs, as requested in #550 and #619. The exact right combination of slotchange and and DOMContentLoaded events, MutationObserver, and connectedCallback, is just too obscure to be usable, and a finishedParsingChildrenCallback doesn't solve the dynamically changing children case.
The platform should provide a reasonable signal for "If I need to process children, when should I do it".
@justinfagnani the childrenChangedCallback is an easy peasy thing to configure in the constructor, if needed, so it's way easier to have and definitively less important than parsedCallback / buildCallback, IMO.
finishedParsingChildrenCallbackdoesn't solve the dynamically changing children case.
anything specific to children doesn't solve much (some component might want to inject its own children without ShadowDOM), and children are super easy to observe already.
The platform should provide a reasonable signal for "If I need to process children, when should I do it".
For one-off setup that is exactly what parsedCallback / buildCallback are being proposed for.
For anything else, if needed, we already have MutationObserver, I don't think we should slow down everything with an implicit mutation observer for children in every custom element.
@justinfagnani The exact right combination of slotchange and and DOMContentLoaded events, MutationObserver, and connectedCallback, is just too obscure to be usable [...]
Very well agreed. Currently it appears like you got to be a total DOM lifecycle, browser DOM implementation and consider-all-the-possible-cases-guru to create robust web components relying on children. @WebReflection called it quantum physics, and that's exactly how you make regular developers resort to frameworks instead of building on top of native technologies.
Did the spec authors not see, or underestimate, or simply ignore this when designing the spec v1?
Hi everyone,
based on the recomendations from this and other posts, we have been able to get around this problem with the folowing steps:
Edit: It was ignorant of me to present this with the words " we have been able to get around this problem" because our solution relies on the requestAnimationFrame which is going to be triggered first when the browser is the able to do so (in chromium after the DOMContentLoaded) which than means that with our solution one should not rely on the DOMContentLoaded but to build the custom "webComponentsReady" event, which delays the JS more and is bad practice. I have been able, as the @cramforce in his post suggested, to get around the problem with the readyState + nextSibling + MutationObserver. And yeah as the @WebReflection already posted, none of the solutions are going to work with the concatenated HTML.
I believe that for my case this would be good enough solution but also believe that this should be solved a bit differently (template + WC ?)
Long story short (new code):
See: https://github.com/w3c/webcomponents/issues/551#issuecomment-431258689
I'd like to underline that in a scenario like the following one, all described techniques would fail.
<!doctype html><html><head><script src="my-el.js"></script></head><body><my-el></my-el></body></html>
The my-el has no sibling and its parent neither, neither the parent parent.
Only connectedCallback would be relevant and yet it won't be granted that the end of the element has been reached so that even in this case parsedCallback / buildCallback would be needed.
TIL browsers _beautify_ the content so that my-el there would have a nextSibling even if not declared, however with this content it won't, and it's still valid:
<!doctype html><html><head></head><body><my-el>
How does all this relate to customized-built-ins that rely on children to set up, like <select is="my-select"> or <ul is="my-list">?
So hands down, this is what we're going to give a shot, following what we were able to extract from this and other posts on the children/connectedCallback topic.
Comments welcome. Yes, we're aware that it will fail in the edge case which @WebReflection mentioned. If anyone sees any other possible edge case, please let us know.
class HTMLBaseElement extends HTMLElement {
constructor(...args) {
const self = super(...args)
self.parsed = false // guard to make it easy to do certain stuff only once
self.parentNodes = []
return self
}
setup() {
// collect the parentNodes
let el = this;
while (el.parentNode) {
el = el.parentNode
this.parentNodes.push(el)
}
// check if the parser has already passed the end tag of the component
// in which case this element, or one of its parents, should have a nextSibling
// if not (no whitespace at all between tags and no nextElementSiblings either)
// resort to DOMContentLoaded or load having triggered
if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
this.childrenAvailableCallback();
} else {
this.mutationObserver = new MutationObserver(() => {
if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
this.childrenAvailableCallback()
this.mutationObserver.disconnect()
}
});
this.mutationObserver.observe(this, {childList: true});
}
}
}
class MyComponent extends HTMLBaseElement {
constructor(...args) {
const self = super(...args)
return self
}
connectedCallback() {
// when connectedCallback has fired, call super.setup()
// which will determine when it is safe to call childrenAvailableCallback()
super.setup()
}
childrenAvailableCallback() {
// this is where you do your setup that relies on child access
console.log(this.innerHTML)
// when setup is done, make this information accessible to the element
this.parsed = true
// this is useful e.g. to only ever attach event listeners to child
// elements once using this as a guard
}
}
customElements.define('my-component', MyComponent)
I've put this into a public gist as well:
@cramforce @WebReflection Could you please take a look and comment?
According to first performance measurements in Chrome done by my colleague @irhadkul the performance drain is minimal (single digit microseconds) when comparing the suggested method with accessing things in connectedCallback outright.
@franktopel things I'd do differently:
WeakSet for the already parsed bitsetup should be instead the connectedCallback of the HTMLBaseElement class, and the class should also have a childrenAvailableCallback no-op (or actually ignore everything on comnnectedCallback if "childrenAvailableCallback" in this is false)childrenAvailableCallback will ever be calledchildrenAvailableCallback callback it doesn't backfire forever.Accordingly, this is how I'd go, or what makes sense to propose as standard.
const HTMLParsedElement = (() => {
const DCL = 'DOMContentLoaded';
const init = new WeakSet;
const isParsed = el => {
do {
if (el.nextSibling)
return true;
} while (el = el.parentNode);
return false;
};
const cleanUp = (el, observer, onDCL) => {
observer.disconnect();
el.ownerDocument.removeEventListener(DCL, onDCL);
parsedCallback(el);
};
const parsedCallback = el => el.parsedCallback();
return class HTMLParsedElement extends HTMLElement {
connectedCallback() {
if ('parsedCallback' in this && !init.has(this)) {
init.add(this);
if (document.readyState === 'complete' || isParsed(this))
// ensure an order via a micro-task so that
// parsedCallback is always after connectedCallback
Promise.resolve(this).then(parsedCallback);
else {
// the need a DOMContentLoaded case
const onDCL = () => cleanUp(this, observer, onDCL);
this.ownerDocument.addEventListener(DCL, onDCL);
// the early case still good to setup one
const observer = new MutationObserver(changes => {
changes.some(record => {
if (record.addedNodes.length) {
cleanUp(this, observer, onDCL);
return true;
}
});
});
// we are interested in the element nextSibling so
// lets observe its parent instead of its own nodes
observer.observe(this.parentNode, {childList: true});
}
}
}
};
})();
Observing the parentNode still might hide shenanigans but at least is its only direct container and not the whole document so it's IMO most likely a more reliable approach.
Eventually, the observer could be configured as {childList: true, subtree: true} and the if should check if (isParsed(this)) instead of checking record.addedNodes.length.
Right ... little variation that uses a WeakMap instead and it's also observing children for all occasions
const HTMLParsedElement = (() => {
const DCL = 'DOMContentLoaded';
const init = new WeakMap;
const isParsed = el => {
do {
if (el.nextSibling)
return true;
} while (el = el.parentNode);
return false;
};
const cleanUp = (el, observer, ownerDocument, onDCL) => {
observer.disconnect();
ownerDocument.removeEventListener(DCL, onDCL);
init.set(el, true);
parsedCallback(el);
};
const parsedCallback = el => el.parsedCallback();
return class HTMLParsedElement extends HTMLElement {
connectedCallback() {
if ('parsedCallback' in this && !init.has(this)) {
const self = this;
const {ownerDocument} = self;
init.set(self, false);
if (ownerDocument.readyState === 'complete' || isParsed(self))
Promise.resolve(self).then(parsedCallback);
else {
const onDCL = () => cleanUp(self, observer, ownerDocument, onDCL);
ownerDocument.addEventListener(DCL, onDCL);
const observer = new MutationObserver(changes => {
if (isParsed(self)) {
cleanUp(self, observer, ownerDocument, onDCL);
return true;
}
});
observer.observe(self.parentNode, {childList: true, subtree: true});
}
}
}
get parsed() {
return init.get(this) === true;
}
};
})();
I think I'll publish this one to npm.
@WebReflection
Comments on your comments:
in empty custom elements a childrenAvailableCallback, as the name implies, doesn't make much sense anyway. As far as I can see you can simply extend HTMLElement instead of HTMLBaseElement in that case. At the end of the day, this approach is meant to solve the problem that arises from children being unavailable when connectedCallback triggers.
Then how would you address asynchronous adding of child elements? Probably the right approach here would be to pass a cleanUp _callback_ to childrenAvailableCallback.
What is still open with this solution is to adjust it for use in customized-built-ins that have expectations about their children.
Btw, I was also considering publishing this on npm, but probably it'll gain more traction if you do it.
@WebReflection I saw you're quick: https://github.com/WebReflection/html-parsed-element
I wouldn't reject attribution, neither would @irhadkul I assume :)
well, if any of you want I can include attributions but just to be clear, this is what HyperHTMLElement does since about ever, using a timeout instead of MutationObserver for better compatibility (down to IE9) and without guarding all calls to connected/attributeChanged before the created() is invoked.
The html-parsed-element is an alternative that might become handy for those not interested in HyperHTMLElement.
I will still link to this ticket in there so again, I don't mind adding anyone in here as contributor 👋
also @franktopel ...
this approach is meant to solve the problem that arises from children being unavailable when connectedCallback triggers.
My approach solves every issue. When parsedCallback is invoked you will have children in there, if any. If you want to listen to further mutations to children, just add your own Mutation Observer.
Then how would you address asynchronous adding of child elements?
You don't . You disconnect the observer too so that's not your intent and also you want to this.childrenAvailableCallback() once, and once only indeed.
Again, the missing bit that is essential is to know when it's safe to handle children or even inject nodes/html. Once we have that, everything else is trivial.
Attribution is definitely welcome. While none of us has done anything even remotely comparable to your work, we surely contributed to the birth of html-parsed-element with the last 10 days' work.
Regarding your comment for better compatibility (down to IE9) I was surprised to find HyperHTMLElement is compatible with every mobile browser and IE11 or greater on https://github.com/WebReflection/hyperHTML-Element
IIR you can test this page and it should work in IE9 too
https://webreflection.github.io/hyperHTML-Element/test/?es5
compatibility is probably for something not fully transpilable but I don't remember what.
Feel free to file a PR for the attribution so I'm sure it's done properly/as you expect.
@WebReflection Something like this would certainly suffice:
Based off the contributions by @franktopel and @irhadkul.
Should I ever file a PR, it's going to be a real fix/improvement.
Btw, how does attributeChangedCallback() behave with respect to children? We have a tabs component that is controlled via a data-active-tab attribute in combination with an attributeChangedCallback case.
@franktopel the attributeChangedCallback is triggered as soon as the beginning of the node is known, AKA the opening tag.
Differently from connectedCallback or whatever children watcher we want, the attributeChangedCallback will never trigger when one attribute is known but another one isn't ,because attributeChangedCallback is consistent in triggering when all attributes on the opening tag are known, instead of randomly in the wild without any signal all nodes are known, which is what my proposal addresses.
P.S. @franktopel attributions are live
@WebReflection So in that case we (our team) have the exact same problem with attributeChangedCallback as well.
the attributeChangedCallback is triggered as soon as the beginning of the node is known, AKA the opening tag. How is that ever useful at all? What would a developer ever want to do at that point in time, without being able to access the element's content?
@franktopel it's useful for empty nodes with shadow dom, and not much else indeed.
With parsedCallback you can setup sure that attributes are there as well.
Meanwhile, if you react to attributeChangedCallback, if this.parsed is false you should queue the operations if important for the component state.
const attributeChanged = new WeakMap;
class MyEl extends HTMLParsedElement {
parsedCallback() {
// setup the node, you have access to all its content/attributes
// then ...
const changes = attributeChanged.get(this);
if (changes) {
attributeChanged.delete(this);
changes.forEach(args => this.attributeChangedCallback(...args));
}
}
attributeChangedCallback(...args) {
if (!this.parsed) {
const changes = attributeChanged.get(this) || [];
if (changes.push(args) === 1)
attributeChanged.set(this, changes);
return;
}
// the rest of the code
}
connectedCallback() {
// here you can safely add listeners
// or set own component properties
this.live = true;
}
disconnectedCallback() {
// here you can safely remove listeners
this.live = false;
}
}
Well, we're not using shadow DOM at all. We just need to replicate Swing controls for the web, for a huge migration project. And currently it needs to support IE 11 mainly. And it has to work for the next 10 to 15 years, without huge update efforts, like you'd have with using a framework like Angular or React. That's why the company decided to use native web components.
So you're saying the whole spec has been designed around a very limited use edge case?
Also, we have been using attributeChangedCallback on a tabs component to set the initially active/visible tab, so this must again have been an issue of it working in the upgrade case, but not in the parsing case. What a mess, again!
Wouldn't it be an even better approach to dispatch a ComponentContentLoaded custom event? That would solve the attributeChangedCallback problem as well, and it would make it so outside elements and components can attach a listener to that event (if they rely on it).
We could even have all components register at a central service in their constructor on creation, and have that same service emit a global ComponentsReady event as soon as all registered component instances have emitted their ComponentContentLoaded event.
You can dispatch any event you want from parsedCallback ...having hybrid callbacks and events feels inconsistent with the API , imo
Most helpful comment
As a data point, AMP's custom element base class has a custom callback called
"buildCallback"named to signal the point when custom elements should be safe to "build" there child structure.This is currently implemented as:
connectedCallbackfirednextSiblingFor the vast majority of elements this is strictly a better time to do initialization than
connectedCallback. CurrentlyconnectedCallbackmay be called with and without children present. That kind of racy behavior leads to bugs all over the place. Requiring use of MutationObserver for cases where actual mutations are unexpected is a really bad programming model and prone to buggy code.Of course, some custom elements must be initialized before children are parsed. This is primarily the case for container elements that may have large amounts of child nodes and waiting for all of them to parse would break streaming rendering. This needs to continue to be supported but that isn't a good argument that there shouldn't be a shortcut for the the more common use case.