WICG / webcomponents

Web Components specifications
Other
4.37k stars 371 forks source link

Need a callback for when children changed or parser finished parsing children #809

Open rniwa opened 5 years ago

rniwa commented 5 years ago

There appears to be a strong desire for having some callback when the parser finishes parsing children or when a new child is inserted or an old child is removed.

Previously, children changed callback was deemed too expensive but I'm not certain that would necessarily be the case given all major browser engines (Blink, Gecko, and WebKit) have builtin mechanism to listen to children changes.

I think we should consider adding this callback once for all. As much as I'd like to make sure custom elements people write are good, developer ergonomics here is quite terrible, and people are resorting to very awkward workarounds like attaching mutation observers everywhere, which is arguably far slower.

Jamesernator commented 5 years ago

Not sure if this is possible but would it be possible to simply capture the value of childrenChangedCallback immediately after the element is constructed? Then one could easily enable an optimization that doesn't bother watching the children at all if the callback isn't actually provided.

(This would be similar to the iterator.next changes in for-of loops that .next is only captured once at the start of the loop).

EDIT: It looks like the other methods already work like this by capturing the methods off the prototype (TIL).

rniwa commented 5 years ago

All custom element callbacks are retrieved at the time of definition so such an optimization is already possible. See step 10 of https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define

annevk commented 5 years ago

I support adding a children changed callback as that is indeed a primitive the DOM Standard provides and specifications and implementations use. (Firefox also has internal synchronous mutation observers, but nothing in the platform relies on them, though Firefox does currently use them for the output element.)

Before we add this however I'd like https://github.com/whatwg/dom/pull/732#pullrequestreview-215166470 solved. In particular, the primitive I referenced above isn't defined that well and some refactoring is needed. It'd be good to settle on the exact shape in private first before exposing it publicly.

(Note that this is effectively #550 btw.)

domenic commented 5 years ago

As with #550, I do not support this if it is parser-specific. If it is just a CEReactions-time MutationObserver that only custom elements can install, then I'm open to investigating. In that case I think we should also seriously consider un-deprecating DOMNodeRemoved/DOMNodeInserted as an API shape, since that already exists in some browsers and we've needed to spec it properly for some time.

justinfagnani commented 5 years ago

If it is just a CEReactions-time MutationObserver that only custom elements can install

I would love it to be a bit more than that, in that it observes projected children as well. I know that's covered slotchange, but currently you need to combine slotchange and a MO to implement "effective children changed".

rniwa commented 5 years ago

I don't think observing the assigned slot should be conflated with this API.

rniwa commented 5 years ago

A common ask that keeps coming up is the ability to know when a child node has been inserted & updated: e.g. https://github.com/w3c/webcomponents/issues/765, https://github.com/w3c/webcomponents/issues/619, https://github.com/w3c/webcomponents/issues/615

To do this, just knowing when a child is inserted or removed isn't quite enough due to async / out-of-order upgrading. Perhaps we need both childChangedCallback and childDefinedCallback the latter of which gets called when a child node is inserted & upgraded / defined.

Also see https://github.com/whatwg/dom/issues/662

caridy commented 5 years ago

@rniwa do you foresee this as something that can be observed in a custom element or a more low level API that can be applied to any element? It is not clear from the title or description.

rniwa commented 5 years ago

@rniwa do you foresee this as something that can be observed in a custom element or a more low level API that can be applied to any element? It is not clear from the title or description.

I think this needs to be a custom element reaction callback.

caridy commented 5 years ago

I think this needs to be a custom element reaction callback.

That's probably limiting. I feel that this is in the same boat as the connect/disconnect reaction that we were asking for last week during the F2F, something a lot more generic.

Now, if we were to add a new reaction callback, will that callback be invoked even when the CE is not connected to the DOM? e.g.:

const ce = document.createElement('x-foo');
ce.appendChild(document.createElement('p'));
rniwa commented 5 years ago

I think this needs to be a custom element reaction callback. That's probably limiting. I feel that this is in the same boat as the connect/disconnect reaction that we were asking for last week during the F2F, something a lot more generic.

For general nodes, you can just use MutationObserver. If you wanted it sooner, then that's a discussion that has to happen in https://github.com/whatwg/dom/issues/533

Now, if we were to add a new reaction callback, will that callback be invoked even when the CE is not connected to the DOM?

You mean when it's not connected to a document? If so, then yes. There is no reason to restrict this childChangedCallback to the connected nodes.

franktopel commented 5 years ago

Remember the imo much more important part remains the childrenParsedCallback. Obviously that wouldn't make too much sense when the CE is not connected to the DOM. Currently CE only work in the upgrade case if they rely on children for setup.

Also, for a childChangedCallback we already have a working tool, MutationObserver. Why would we need an additional callback?

rniwa commented 5 years ago

Let first state that the time at which the parser had finished running isn't a great way to do anything because DOM is inherently dynamic. Any node can be inserted or removed after the parser had finished parsing its contents, not to mention that HTML parser could later insert more children via adoption agency, etc...

The reason we want to add childChangedCallback is ergonomics for the same reason we have attributeChangedCallback even though attribute changes can be observed via MutationObserver as well.

Now, custom elements have unique characteristics that child elements may be dynamically upgraded later so just knowing when a child is inserted may not be enough for a parent element to start interacting with a child element. This is a rather fundamental problem with the current custom elements API. We need some way for a parent to discover a child custom element when it becomes well defined.

annevk commented 5 years ago

@rniwa how is whenDefined() not adequate for that?

franktopel commented 5 years ago

Any node can be inserted or removed after the parser had finished parsing its contents

I'm not interested in that case. Imagine you want to have an element <table is="data-table"> and you need to set it up - how are you ever going to do this without having guaranteed access to all children? Many web projects still generate static HTML code on the serverside using classic template engines like Thymeleaf etc. This statically generated content inside a webcomponent needs to be reliably accessible for the webcomponent at some point - and this point should definitely be a lifecycle callback, not the funky stuff that frameworks like Google AMP do, which led to the creation of https://github.com/WebReflection/html-parsed-element

In the course of the discussion back in the days I created a gist that sums up the problem and suggests a solution that served as a basis for the development of the aforementioned package. https://gist.github.com/franktopel/5d760330a936e32644660774ccba58a7

annevk commented 5 years ago

As already explained in multiple comments at the beginning of #551 that's not a pattern I want to encourage by adding API surface for it as it'll break down whenever someone dynamically uses such an element. I'm not sure why @rniwa mentioned the parser again. If we really want to reopen that discussion it should be in a separate issue as it's unlikely to get agreement, whereas children changed is an existing low-level primitive that would be vastly easier to get agreement on exposing.

franktopel commented 5 years ago

There appears to be a strong desire for having some callback when the parser finishes parsing children

This is exactly what this issue addresses, unfortunately it mixes it up with

or when a new child is inserted or an old child is removed.

which is a completely different problem. I really think there is a strong necessity to solve the first problem first, because the second problem can already be tackled with using a MutationObserver. While that may not be as convenient as having a childrenChangedCallback, it definitely works and does so without going ten extra miles just to find the right time via deduction the type of which

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});
}

does.

Please note that this is also explicitly mentioned in the entry post:

[...] and people are resorting to very awkward workarounds like attaching mutation observers everywhere

justinfagnani commented 5 years ago

which is a completely different problem.

I don't think parser finishing and dynamic children are different problems at all. The parser adding nodes is just a sub-problem of dynamic child changes and a single API can handle both cases.

The fact that you can use MutationObservers after parsing is complete doesn't solve the current issue that developers have to piece together several APIs in order to know when their children change.

rniwa commented 5 years ago

I'm not interested in that case. Imagine you want to have an element table is="data-table" and you need to set it up - how are you ever going to do this without having guaranteed access to all children?

Components that don't support updating its appearance upon dynamic DOM tree change is outside the scope of custom elements API. We don't intend to address such a use case.

To answer @annevk's comment about why I brought up the parser again: I think there is legitimate scenarios in which the most natural solution authors think of is finishedParsingChildrenCallback. As far as I dissected the problem space, I think the most common complain there is that out-of-order upgrades makes it impossible to know when a parent can start interacting with children. I think this is an unique requirement that built-in elements don't have. So while I tend to agree we don't want to add that exact callback, we may need something like childDefinedCallback as I suggested above to address the underlying use cases of issues which motivated folks to request finished parsing callback.

franktopel commented 5 years ago

Components that don't support updating its appearance upon dynamic DOM tree change is outside the scope of custom elements API.

Can you please give reference on this? I assume such a heavy-weight decision is well-reasoned and -documented. What exactly is in scope of the custom elements API?

annevk commented 5 years ago

@rniwa it seems to me that the combination of childrenChangedCallback, customElements.whenDefined(), plus some bookkeeping is all you need for that. Though perhaps if the bookkeeping gets too complicated there is some kind of shortcut we could offer if libraries all end up with something similar. (Seems like something user land should figure out the pattern first for though.) Also, @domenic has a point though that https://github.com/whatwg/dom/issues/305 perhaps should be flushed out first given that we're likely not able to get rid of mutation events. Nevertheless, iIt might still make sense to offer a childrenChangedCallback given its similar timing and more idiomatic custom element API.

@franktopel it was decided early on that we wanted to encourage custom elements that behave equivalently to built-in elements and would therefore not add hooks that built-in elements do not have. It might be a little tricky to find a definitive reference for that, but that's best discussed separately from this thread in a new issue.

justinfagnani commented 5 years ago

@rniwa it seems to me that the combination of childrenChangedCallback, customElements.whenDefined(), plus some bookkeeping is all you need for that. Though perhaps if the bookkeeping gets too complicated there is some kind of shortcut we could offer if libraries all end up with something similar. (Seems like something user land should figure out the pattern first for though.)

To give some color about what I've been doing for this situation: I generally fire events from children when they're ready to interact with an ancestor, and have the child wait for the parent to be ready to receive the event with customElements.whenDefined(). This seems to be simpler code usually and avoids tightly coupling to the exact structure of the DOM (works with grandchildren as well).

WebReflection commented 5 years ago

The fact that you can use MutationObservers after parsing is complete

I think current work around is using MutationObserver on constructor, hence before the parsing is complete.

I generally fire events from children when they're ready to interact with an ancestor

That works only if children are Custom Elements.

A <sortable-list> might be a container for an UL and some LI, and knowing when the content is ready is crucial when it comes to setup.

Imagine the server producing the following based on some data:

<sortable-list>
  <ul>
    <li>a</li>
    <li>b</li>
  <ul>
</sortable-list>

If you define upfront the sortable-list component, and you don't want to use ShadowDOM, which shouldn't be mandatory to setup Custom Elements, the only way to do that is to use a hack via html-parsed-element or similar, or to use MutationObserver within the constructor so that the component will inevitably flick due asynchronous nature of the MO.

I think @rniwa here nailed the issue me, and others, are complaining about.

I think there is legitimate scenarios in which the most natural solution authors think of is finishedParsingChildrenCallback.

Anything else would result in probably nice to have, but not an answer to the real issue.

The fact built-in elements don't have such lifecycle is misleading, 'cause a <select> internally setup likely once, instead of N times synchronously per each discovered <option> with it.

We, users, simply don't get to know when that setup should happen, which is independent of the usage of MO in case the component should be able to react on added/removed nodes, 'cause these are not mutually exclusive scenarios.

annevk commented 5 years ago

Did you test <select>? As far as I know that's not true. (Not even clear to me how that would work if you dynamically append to it.)

WebReflection commented 5 years ago

Did you test <select>? As far as I know that's not true

OK then, let's talk about other builtins where some of those ship with Shadow DOM included and get rendered only once they are fully known (input, progress, etc)

I've never seen an input flicking while setting up its own SD or styles, and same happens if you use Shadow DOM in the constructor.

Although, does Shadow DOM need to be mandatory to have useful custom elements?

And how/when should we setup its internal events/behaviors?

In these cases a "parsed" callback would help. Maybe "parsed" is not the right term, but it's semantic with what we'd like to have.

WebReflection commented 5 years ago

P.S. I think GitHub broke its own comment parsing, which is why my previous message looks like that 😢

annevk commented 5 years ago

In those cases there's nothing to parse right? The element implementation has all the necessary bits already available? Your custom element input can also create a whole bunch of div elements with styles synchronously and inject them in a shadow tree at creation time.

WebReflection commented 5 years ago

so you are saying Shadow DOM should be mandatory to reliably setup elements without flicks, right?

otherwise, can you please explain to me how to setup this, generated from the server, and pre-registered as component on the client, without using Shadow DOM? Thanks

<sortable-list>
  <ul>
    <li>a</li>
    <li>b</li>
  <ul>
</sortable-list>
annevk commented 5 years ago

I don't see how that case is equivalent to <input>, which has no (same node tree) children.

I'm a little worried we're dragging this too far off-topic, but could you elaborate on what kind of flickering you are encountering? Is there a demo somewhere? Mutation observer callbacks should in theory always happen before rendering. You might potentially see more elements get added on a per frame basis, but it's not clear to me how that is different from incremental rendering of built-in elements.

WebReflection commented 5 years ago

Mutation observer callbacks should in theory always happen before rendering.

I didn't know this, meaning the following pattern should grant the element has been parsed, right?

constructor() {
  super();
  new MutationObserver((, mo) => {
    mo.disconnect();
    this.finishedParsingChildrenCallback();
  }).observe(this, {childList: true});
}

if that's the case, I might update my html-parsed-element library then, yet @rniwa concern on abusing MO all over will still be valid, and I wish myself there was a finishedParsingChildrenCallback or similar method for CE instantiation.


edit also please note maybe a CE doesn't have children so that such method might never trigger, leaving the element partially initialized.

WebReflection commented 5 years ago

Another reason for desiring a reliable entry point that works no matter the circumstances, is the following one: a basic star rate component.

// <star-rate value=1-5>
customElements.define('star-rate', class extends HTMLElement {
  attributeChangedCallback(name, old, value) { this[name] = value; }
  get observedAttributes() { return ['value']; }
  get value() {
    return this.getAttribute('value');
  }
  set value(value) {
    let input = this.querySelector(`input[value="${value}"]`);
    let changed = false;
    if (input && !input.checked)
      input.checked = (changed = true);
    else {
      input = this.querySelector(`input:checked`);
      if (input && input.checked)
        input.checked = !(changed = true);
    }
    if (changed)
      input.dispatchEvent(new CustomEvent('change', {bubbles: true}));
  }
  connectedCallback() {
    this.addEventListener('change', this);
    // âš  the following is unpredictable
    // it might work or not
    const {value} = this;
    if (value)
      this.value = value;
  }
  handleEvent(event) { this[`on${event.type}`](event); }
  onchange(event) {
    const {target} = event;
    this.classList.toggle('checked', target.checked);
  }
});

This basic custom element would like to simply expose its own input value, and it can be represented on the HTML as such (so it's SSR compatible out of the box)

<star-rate value="3">
  <input type="radio" name="star-rate" value="1">
  <input type="radio" name="star-rate" value="2">
  <input type="radio" name="star-rate" value="3">
  <input type="radio" name="star-rate" value="4">
  <input type="radio" name="star-rate" value="5">
</star-rate>

Star rating is one of the most basic and common Web components out there, and while every framework/library would easily provide a way to do exactly what above custom element definition does, the standard way will likely fail because it's unpredictable.

For instance, the CodePen version will always work as expected, because the script is executed after the layout has been generated.

However, defining the custom element upfront will likely fail in Chrome, will probably fail in Firefox, and won't likely set it up in Safari.

Try this page to find out https://webreflection.github.io/test/star-rate/ and refresh on Firefox to see it works sometimes, and not some other times.

This is awkward (to put it gently)

The fact developers don't have an entry point to setup their own components in a reliable way across browsers is one of the reason people still don't go custom elements.

Every alternative for a basic star-rate component would be easier and more reliable, with Custom Elements we always have this chicken/egg issue if the definition is known upfront or not, and if the DOM is already live or not.

The strongest point of Custom Elements without Shadow DOM is SSR capability and graceful enhancement, and this is also the weakest feature in practice.

V0 used to have a createdCallback that was indicating the whole upgrade was done, and IIRC it was OK in that case to check for internal children state, but there's nothing in V1 that helps developers avoid shooting their own foot through the platform.

The ideal scenario

All it would take to actually solve every issue, is to be notified when the closing tag of a DOM node has been encountered (even if automatically closed, or if it's a void element)

I understand the platform never offered that, but it's also clear to me by now it should.

Until we have that information available, frameworks will always win in reliability too, because their declarations land at once, and whenever the component is initialized (mounted/connected/whatever) internal nodes would be already available to perform any sort of action.


I hope this extra example would clarify more why we need some mechanism to setup components.

Best Regards

Jamesernator commented 5 years ago

I didn't know this, meaning the following pattern should grant the element has been parsed, right?

I'll test this later but I doubt it, mutation observer callbacks happen on the microtask queue so I'd imagine if there was flaky internet it could add a couple children, pause parsing to wait for more data and wind up running mutation observer callback microtasks before all children are parsed.

As far as I'm aware the only reliable way to detect end of parsing of a specific element is this thing from earlier in the thread.

jakearchibald commented 5 years ago

@WebReflection your mutation observer hack… can't that still fire before the element is fully parsed? If 50% of the element had been parsed, and rendered, I'd expect a mutation observer callback before render.

jakearchibald commented 5 years ago

Just wanted to add that I want keep wanting the "parsing done" callback, especially for progressive enhancement.

<my-custom-element>
  …some acceptable pre-JS markup that's eventually enhanced…
</my-custom-element>

In cases like this, I want to run JS when parsing is complete, then look at the contents, and use it to initialise the interactive element (perhaps move everything into shadow dom). Edits to element content are ignored after parsing.

I find myself having to do:

<my-custom-element>
  …some acceptable pre-JS markup that's eventually enhanced…
</my-custom-element>
<script>
  {
    const el = document.currentScript.previousElementSibling;
    customElements.whenDefined('my-custom-element')
      .then(() => el.enhance());
  }
</script>
jakearchibald commented 5 years ago

@domenic

In that case I think we should also seriously consider un-deprecating DOMNodeRemoved/DOMNodeInserted as an API shape, since that already exists in some browsers and we've needed to spec it properly for some time.

Alternatively, we could have a { sync: true } option on mutation observers, which would fire immediately rather than on microtask. Then things like childList remain opt-in.

justinfagnani commented 5 years ago

@jakearchibald I don't understand this snippet:

<my-custom-element>
  …some acceptable pre-JS markup that's eventually enhanced…
</my-custom-element>
<script>
  {
    const el = document.currentScript.previousElementSibling;
    customElements.whenDefined('my-custom-element')
      .then(() => el.enhance());
  }
</script>

Why wouldn't .enhance() be called in my-custom-element's constructor?

Dur: sorry. I see, the parser yielding in the middle, the whole point of this thread. I'll leave this up anyway...

domenic commented 5 years ago

Edits to element content are ignored after parsing.

Much of this thread and previous ones is a discussion about how custom elements which have this sort of behavior are discouraged, and so adding an API to make them easier is not desirable for some of us.

I imagine you might be aware of that, but in case others are coming in via Twitter or similar, I thought it'd be good to re-establish that context.

dogoku commented 5 years ago

Much of this thread and previous ones is a discussion about how custom elements which have this sort of behavior are discouraged, and so adding an API to make them easier is not desirable for some of us.

I imagine you might be aware of that, but in case others are coming in via Twitter or similar, I thought it'd be good to re-establish that context.

@domenic Thanks for thinking of people like me which landed on this thread without any context. Can you please share the rationale behind this decision (to discourage such behaviour) or a link to it?

I was going to add my 2 cents, but perhaps knowing the history, will help answer some (of my) questions and avoid the conversation repeating itself. Thanks.

domenic commented 5 years ago

I'd encourage people to read this thread, #550, and #551.

WebReflection commented 5 years ago

@dogoku the TL;DR of current state of the art is that given the following JS:

// your custom element
customElements.define('my-select', class extends HTMLElement {
  finishedParsingChildrenCallback() {
    console.log('ready to go');
  }
});

// the currently needed utility
customElements.define('the-end', class extends HTMLElement {
  connectedCallback() {
    const {parentElement} = this;
    parentElement.removeChild(this);
    if (parentElement.finishedParsingChildrenCallback)
      parentElement.finishedParsingChildrenCallback();
    else
      parentElement.dispatchEvent(new CustomEvent('finishedparsingchildren'));
  }
});

and the following layout:

<my-select>
  <select>
    <option>1</option>
    <option>2</option>
    <option>3</option>
  </select>
  <the-end/>
</my-select>

you would know when your custom element, the one containing the select, is ready to setup itself also based on its content, as you can see in this code pen.

However, not only this awkward "hack" is needed because Custom Elements are missing a callback that tells the component it's OK to set itself up, there's always a chicken-egg issue about when a Custom Element is defined:

The latter case is usually covered by the common situation where you land all custom elements and their content at once, at least via JS, but not necessarily via SSR.

The first point though is problematic, 'cause as soon as you have the definition order of the the-end component after a component that needs it, nothing works as expected.

As example, this other code pen simply defines the the-end component before others, and you won't see anything in the console, hence no setup granted.

The proposed feature here, is to inform a generic Custom Elements when such "end" of its content is reached, so that everyone can have a single entry point to setup components that depends on their content, same as native select, ul, table or other more complex HTML elements do.

There's a missing primitive that requires a lot of overhead per each component because of such missing information/callback/event, and every framework not based on custom elements has an easier life in setting itself up, 'casue the content is already accessible, which is also not always the case for Custom Elements.

As example, if you have a definition in your document head and the element is initialized while its opening tag is encountered, you need a lot of "dancing" to figure out when it's the right time to set anything up.

This is somehow a non-issue when your component uses Shadow DOM, as that's usually done in the constructor, but it becomes a foot gun prone approach when you'd like to have Server Side Render friendly components, and you don't control when these are defined in any hosting site.

I hope this summary helped a little to go back on track with the issue, and any possible solution.

dogoku commented 5 years ago

@WebReflection your star element example from earlier in the thread, already gave me a clear understanding of your point and honestly it's something my team has been struggling with as well (ironically, a custom select element was one of the pain points), often resorting to ugly setTimeout hacks.

What I was trying to learn is the counter argument to this, which might not be immediately obvious if it comes down to performance or some other browser internals. By going through #550 as suggested by Domenic, I found this comment to be the most informative:

As I've numerously stated in various threads & issues, the right approach is for the child to report its insertion to the parent. Waiting for all children to be parsed in order to initialize is never a good idiom because the HTML parser may pause for seconds at a time when there is a network delay.

We used to have code like that in our builtin element implementations and we still do in some cases (due to historical reasons) but almost all such code end up causing bugs. It's simply not the right pattern by which to write an element.

Originally posted by @rniwa in https://github.com/w3c/webcomponents/issues/550#issuecomment-458395228

(P.S: I also wanted to avoid adding noise, which seems I have failed in doing 😅)

WebReflection commented 5 years ago

@dogoku well, yes, if there was at least a contentChangedCallback that would automatically enable access to the Custom Element content (i.e. this.innerHTML or this.textContent, or this.querySelector(...)) we'll have an easier life, as that can help with unexpected flushes of the page and it will expose a precise point in time where you can at least start setting up your component.

After all, even <select> shows their content before their end tag is encountered:

<?php
header('Content-type: text/html; charset=utf-8');
echo '<select>';
for ($i = 0; $i < 10; $i++) {
  echo '<option>'.$i.'</option>';
  if ($i === 5) {
    flush();
    ob_flush();
    sleep(5);
    // explore the select up to 5 options
  }
}
echo '</select>';
?>

So while I personally dream about a contentParsedCallback that would happen independently of the content, as long as the closing tag is either explicitly encountered or automatically closed, anything else that could help setting up reliably a Custom Element would be better than current state.


edit the reason I believe the closing tag callback would be more efficient, is that a custom element could have visibility: hidden up to that point, so that a user prematurely interacting with an element that exposes some default down the stream won't fail when choosing an option, es example, before such stream ends.

<?php
header('Content-type: text/html; charset=utf-8');
echo '<select>';
for ($i = 0; $i < 10; $i++) {
  echo '<option'.($i === 7 ? ' selected' : '').'>'.$i.'</option>';
  if ($i === 5) {
    flush();
    ob_flush();
    sleep(5);
    // you have 5 seconds to change option
  }
}
echo '</select>';
// see the user previous choice replaced by the 7th entry after 5 seconds
?>
kmmbvnr commented 3 years ago

Are there any active plans to fix this?

Currently, I have to define part of my elements inside DOMContentLoaded. That's quite incorrect and don't handles all possible scenarios, but works in my case.

window.addEventListener('DOMContentLoaded', () => {
  window.customElements.define('my-elem', MyElem);
});
Sleepful commented 1 year ago

Someone suggested requestAnimationFrame to callback when parser finished parsing children.

  connectedCallback() {
    window.requestAnimationFrame(()=>{
        this.querySelectorAll("input") // or whatever chlidren-specific operation
    })
  }

Using this method, you will always have all child nodes available.

is this a good solution? seems straightforward and ultra-simple.

Source: https://github.com/WICG/webcomponents/issues/551#issuecomment-1424638706

jakearchibald commented 1 year ago

No. Browsers can render documents during parsing, and rendering includes calling requestAnimationFrame callbacks.

mfreed7 commented 1 year ago

Here's an attempt at a concrete proposal:

Define an event called finishedparsingchildren, whose behavior is:

When a Node node is popped off the stack of open elements of an HTML parser or XML parser, queue an element task on the DOM manipulation task source given the node to fire an event named finishedparsingchildren at node.

I believe that should be relatively straightforward to implement, and should address the use cases discussed in this issue. A few comments/variations:

Thoughts?

rniwa commented 1 year ago

Why an event instead of a custom element reaction callback? What use cases does it serve besides custom elements?

Danny-Engelman commented 1 year ago

We have bypassed this over the years in multiple ways. I plea for Web Component gurus to work on real problems.

https://jsfiddle.net/WebComponents/ar7jengd/

<template id="MY-ELEMENT">
  <img src="" onerror='this.getRootNode().host.parsed("img onerror")'>
  <style onload='this.getRootNode().host.parsed("style onload")'></style>
</template>

<my-element>
  <span></span>
  <b></b>
</my-element>

<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      const templateById = (id) => document.getElementById(id);
        const clone = (id) => templateById(id).content.cloneNode(true);
        super().attachShadow({mode:"open"}).append(clone(this.nodeName));
    }
    connectedCallback() {
      setTimeout(() => this.parsed("timeout"))
      requestAnimationFrame(() => this.parsed("rAF"));
      console.log("connectedCallback");
    }
    parsed(ref){
        console.log("parsed() called by", ref, this.children)
    }
  });
</script>
franktopel commented 1 year ago

We have bypassed this over the years in multiple ways. I plea for Web Component gurus to work on real problems.

If a not-so-uncommon use-case requires one of a multitude of timing-based workarounds, what could prove better there's a real issue here?

Danny-Engelman commented 1 year ago

I am not denying the issue, but the last real contribution in this thread was september 2019

If this really was a Big issue, it would have been fixed.