custom elements & connectedCallback() : wait for parent node to be available before firing a function
Asked Answered
O

3

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 !

Ogden answered 12/10, 2019 at 13:13 Comment(2)
Possible duplicate of textContent empty in connectedCallback() of a custom HTMLElementParadies
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!Suited
P
3

Long answer moved to a single StackOverflow answer where all updates and new knowlegde is kept: https://mcmap.net/q/55574/-web-components-accessing-innerhtml-in-connectedcallback

Puma answered 13/10, 2019 at 9:13 Comment(2)
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://mcmap.net/q/55575/-what-is-the-difference-between-settimeout-fn-0-and-settimeout-fn-1Jabiru
I have added a NoteParadies
F
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!

Filibertofilibuster answered 12/10, 2019 at 15:28 Comment(7)
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: #48499081Paradies
An why do you give a totally different (and better) answer 2 hours earlier?? #48664178Paradies
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.Filibertofilibuster
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.Filibertofilibuster
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 <script> tag at the start of the HTML and you get different behaviourParadies
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.Filibertofilibuster
Interesting additional read on when things happen: #44712879Paradies
A
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();
  }
}
Apteryx answered 26/2, 2022 at 1:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.