4

I'm using custom elements, which are very nice. But I'm facing a problem :

When the connectedCallback() function is called, it seems that the node is not yet at its place in the DOM, thus I cannot access its parents - and I need them.

class myElement extends HTMLElement{
    constructor() {
        super();
        this.tracklist =    undefined;
    }
    connectedCallback(){
        this.render();
    }
    render(){

        this.tracklist = this.closest('section');

        // following code requires this.tracklist!
        // ...
    }

window.customElements.define('my-element', myElement);

How could I be sure the parent nodes are accessible before calling render() ?

Thanks !

gordie
  • 1,637
  • 3
  • 21
  • 41
  • 1
    Possible duplicate of [textContent empty in connectedCallback() of a custom HTMLElement](https://stackoverflow.com/questions/48498581/textcontent-empty-in-connectedcallback-of-a-custom-htmlelement) – Danny '365CSI' Engelman Oct 13 '19 at 08:23
  • This is a bad component design. You should not rely on your component residing in a certain DOM context. You could for example append it to a `documentFragment` - in this case it wouldn't even have *any* parent nodes! – connexo Sep 17 '20 at 19:17

3 Answers3

3

It is intended behavior:

connectedCallback fires on the opening tag, its lightDOM was not parsed yet. (unless the CE.define runs after the DOM is parsed)

connectedCallback does not mean your element is or is not fully parsed.
Custom Elements does not have a parsedCallback method like all Tools (Lit, Stencil, etc) have

See all the answers at:

TL;DR;

A workaround to delay your render method:

 connectedCallback(){
     setTimeout(this.render , delay ); 
     // delay=1 will allow more lightDOM parsing than 0 (none) 
 }

This gives the Browser time to parse more nodes (your lightDOM nodes) And executes when the Event Loop is empty.

Note: this works fine for less than 500 to 1000 Nodes approx. Your milage may vary if you have (too) many Nodes in lightDOM. You then need a longer timeout, or add code that explicitly checks for all lightDOM to be created. See: https://github.com/WICG/webcomponents/issues/809

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • I've had cases where even that's not enough. Sometimes setTimeout (with 0ms or without argument) would be enough, some other times it's not enough of a delay. When I set the argument to 1ms, I never have the issue. This seem to go along with this answer from another topic: https://stackoverflow.com/a/16182869/10294974 – Max Jul 03 '23 at 10:10
  • I have added a Note – Danny '365CSI' Engelman Jul 03 '23 at 12:30
0

It seems that the connectedCallback cannot access other elements in relation to itself when it hasn't been parsed yet. This kind of makes sense if you consider that a custom-element has to be able to live anywhere in the DOM without being dependent on another element. So if there were no parent to be selected, the element would probably not work properly.

A way to do this is to modify the render method to take an argument which will set the tracklist property dynamically to the custom element. Then select the my-element element from the DOM and look for the section.

Then use the customElements.whenDefined method to connect the section and my-element together whenever the custom element is ready. This method returns a Promise that resolves whenever the custom element is defined and gives you the ability to execute a callback.

See example below:

// Do something whenever the element is ready.
window.addEventListener('load', function() {

  // Wait for the document to load so the DOM has been parsed.
  window.customElements.whenDefined('my-element').then(() => {
    const myElement = document.querySelector('my-element');

    // Only do something if the element exists on the page.
    if (myElement !== null) {
      const tracklist = myElement.closest('section');
      myElement.render(tracklist);
      console.log(myElement.tracklist);
    }

  });

});

// Create element.
class myElement extends HTMLElement{
    constructor() {
        super();
        this.tracklist = null;
    }
    render(tracklist){
        this.tracklist = tracklist;
        // following code requires this.tracklist!
        // ...
    }
}

// Define element.
window.customElements.define('my-element', myElement);
<section>
  <my-element></my-element>
</section>

If I have been unclear or you have questions, please let me know.

Have a nice day!

Emiel Zuurbier
  • 19,095
  • 3
  • 17
  • 32
  • *It seems that the connectedCallback cannot access other elements in relation to itself.* You might want to add "when it hasn't been parsed yet" to that bold statement. See: https://stackoverflow.com/questions/48498581/textcontent-empty-in-connectedcallback-of-a-custom-htmlelement – Danny '365CSI' Engelman Oct 13 '19 at 08:25
  • An why do you give a totally different (and better) answer 2 hours earlier?? https://stackoverflow.com/questions/48663678/how-to-have-a-connectedcallback-for-when-all-child-custom-elements-have-been-c – Danny '365CSI' Engelman Oct 13 '19 at 09:23
  • Thanks for the feedback, I've added your quote. About the former comment: although both cases seem similar, OP wants to select a parent and the other thread tries to select a child from a custom element. The `slot` element is also a solution to the problem here, and if you think it would be better, I'd be happy to add it. – Emiel Zuurbier Oct 13 '19 at 12:53
  • But my initial emphasis is on creating custom elements which do not worry about other elements in the DOM, or even its own content. You pointed out the missing `parsedCallback` method. And the reason for its absence makes sense. Native elements don't have this behavior either and don't modify siblings, parents, textContent or any other nodes when they are initialized. It is up to you to modify it whenever you need it and can do that outside of the elements. However this is my conclusion taken from the workings of native and custom elements. I'd be happy to hear your thoughts about this. – Emiel Zuurbier Oct 13 '19 at 12:59
  • Another issue (after looking more closely at your code) whenDefined is executed once the element is defined in the customElementRegistry.. so what happens if there is no ``my-element`` in the DOM? Problem with JSFiddle (and maybe with inline code here as well) is that the JS code is executed **after** the HTML code. (in JSFiddle) Put all JS in a – Danny '365CSI' Engelman Oct 14 '19 at 08:55
  • Very true, as that is with any kind of JS that selects elements from the DOM **before** the HTML in the body. Wrapping the code inside the `whenDefined` callback in a `window.onload` listener and checking if `my-element` exists would solve that problem. If OP decides to include the JS at the top of the document, then he would have to wait for the `load` or `DOMContentLoaded` event no matter what. – Emiel Zuurbier Oct 14 '19 at 09:08
  • Interesting additional read on when things happen: https://stackoverflow.com/questions/44712379/when-does-settimeout-start-executing-in-a-inline-script – Danny '365CSI' Engelman Oct 14 '19 at 11:22
0

I haven't tested this out but seems like a Promise might work:

// DomReady.js
class DomReady extends HTMLElement {
  constructor() {
    super();
    this.domReadyPromise = new Promise(resolve => (this.domReadyResolve = resolve));
  }

  connectedCallback() {
    this.domReadyResolve();
  }

  domReady() { return this.domReadyPromise; }
}
// ParentCustom.js
class ParentCustom extends DomReady {
  connectedCallback() {
    super.connectedCallback();
    ...
  } 
}
// ChildCustom.js
class ChildCustom extends HTMLElement {
  async connectedCallback() {
    await document.querySelector('parent-custom').domReady();
  }
}
underblob
  • 946
  • 9
  • 10