admin管理员组

文章数量:1355093

So I'm trying to build a custom ponent using vanilla javascript, which will do certain things depending on the number of children it has, meaning it has to count said children

If I have the following markup (where the custom ponent is called "my-ponent")

<my-ponent>
  <div></div>
  <!-- ...arbitrary number of child elements -->
</my-ponent>

And the following javascript code in the <head></head> to ensure it's loaded before the <body></body> is parsed

class MyComponent extends HTMLElement {

  constructor(){
    super()
    this.children.length
    //do stuff depending on the number of children
  }

  //or

  connectedCallback () {
    this.children.length
    //do stuff depending on the numbre of children
  }

}

customElements.define("my-ponent",MyComponent)

this.children.length will return 0 in both cases, despite the elements showing on the screen afterwards, and being able to inspect the custom element on the console and get the expected number of children with Element.children.length. I suppose that this means the children elements are not yet available at the time the constructor nor the connectedCallback are run.

Is there any way to specify in my element's class definition a function that will trigger when the children elements bee available, so that I can do stuff with them? I was hoping for a "childElementsReady" callback or something similar, but I guess that it doesn't exist. I don't know if there's a really obvious way to deal with this that I'm just missing, because this seems like something that I should be able to do relatively easily

So I'm trying to build a custom ponent using vanilla javascript, which will do certain things depending on the number of children it has, meaning it has to count said children

If I have the following markup (where the custom ponent is called "my-ponent")

<my-ponent>
  <div></div>
  <!-- ...arbitrary number of child elements -->
</my-ponent>

And the following javascript code in the <head></head> to ensure it's loaded before the <body></body> is parsed

class MyComponent extends HTMLElement {

  constructor(){
    super()
    this.children.length
    //do stuff depending on the number of children
  }

  //or

  connectedCallback () {
    this.children.length
    //do stuff depending on the numbre of children
  }

}

customElements.define("my-ponent",MyComponent)

this.children.length will return 0 in both cases, despite the elements showing on the screen afterwards, and being able to inspect the custom element on the console and get the expected number of children with Element.children.length. I suppose that this means the children elements are not yet available at the time the constructor nor the connectedCallback are run.

Is there any way to specify in my element's class definition a function that will trigger when the children elements bee available, so that I can do stuff with them? I was hoping for a "childElementsReady" callback or something similar, but I guess that it doesn't exist. I don't know if there's a really obvious way to deal with this that I'm just missing, because this seems like something that I should be able to do relatively easily

Share Improve this question asked Jul 21, 2021 at 19:11 super potionsuper potion 3753 silver badges13 bronze badges 2
  • Search SO for connectedCallback and setTimeout, I have posted multiple answers – Danny '365CSI' Engelman Commented Jul 22, 2021 at 19:51
  • I found your answer that involves setTimeout and been playing around with the possible options, and thats one way to do it, but relying on an arbitrary amount of time to pass and hoping that by then everything is loaded is scary tbh, even if it seems like it will work most of the time – super potion Commented Aug 2, 2021 at 19:23
Add a ment  | 

4 Answers 4

Reset to default 5

A MutationObserver is the best way to handle this. You can set one up in connectedCallback to observe changes to the Light DOM - in this case it's enough to observe childList only:

class MyElement extends HTMLElement {
  constructor() {
    super();

    this.onMutation = this.onMutation.bind(this);
  }

  connectedCallback() {
    // Set up observer
    this.observer = new MutationObserver(this.onMutation);

    // Watch the Light DOM for child node changes
    this.observer.observe(this, {
      childList: true
    });
  }

  disconnectedCallback() {
    // remove observer if element is no longer connected to DOM
    this.observer.disconnect();
  }
  
  onMutation(mutations) {
    const added = [];

    // A `mutation` is passed for each new node
    for (const mutation of mutations) {
      // Could test for `mutation.type` here, but since we only have
      // set up one observer type it will always be `childList`
      added.push(...mutation.addedNodes);
    }
    
    console.log({
      // filter out non element nodes (TextNodes etc.)
      added: added.filter(el => el.nodeType === Node.ELEMENT_NODE),
    });
  }
}

customElements.define('my-element', MyElement);

Here onMutation will be called every time nodes are added to the Light DOM so you can handle any set up here.

Note that, depending on the nodes in the Light DOM, onMutation can be called more than once when the element is connected to the DOM so it's not possible to say that all the children are 'ready' at any point - instead you must handle each mutation as it es in.

I write this reply to my own question because I found out that this is an useful way to watch for added children when you have a shadow dom, so hopefully it can help anyone in that situation, but lamplightdev's answer is the most plete one since it works both when you use a shadow dom or not, so look up his answer as well

If your custom element makes use of the shadow dom, you can do this:

class MyComponent extends HTMLElement {

  childAddedCustomCallback () {
    let watchedSlot = this 
      /*"this" here is not the custom element, but a slot that the custom
      element will have embedded in its shadow root, to which this function 
      will be attached as an event listener in the constructor*/
    
    let children = watchedSlot.assignedElements()
    let numberOfChildren = children.length
    
    //do stuff depending on the number of children
  }

  constructor(){
    super()
    let shadowRoot = this.attachShadow({mode:"open"})
    shadowRoot.innerHTML = "<slot></slot>"

    let slotToWatch = shadowRoot.querySelector("slot")
    slotToWatch.addEventListener("slotchange",this.childAddedCustomCallback)
  }

}

customElements.define("my-ponent",MyComponent)

With this, every time you add a child to the custom element, it will reflect to the unnamed slot, and the slot's event listener will trigger the callback when that happens, giving you a reliable and clear way to access the children of the element as soon as they're available

This has the downside of excecuting n times if you add n children to the custom element, e.g. if you have the following markup:

<my-ponent>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
</my-ponent>

Instead of excecuting once when the last child element is added, it will excecute 4 times (one for every child), so beware of that

Just some other relevant information. Sometimes when you are cloning or using importNode the child nodes might already be inserted and the mutation observer won't catch them, in this situation you will have to look through this.children to find currently inserted child nodes.

Here is a relevant ment from GH Issues:

So a custom element with expectations on children will need to:

  • Check if has children in connectedCallback, do stuff
  • Set up a mutation observer and do stuff when nodes are added (which will happen in the case of upgrades).
  • Disconnect the mutation observer in disconnectedCallback

Finally here is a simple example with the Mutation Observer, just run it and look at console:

<script>
  window.customElements.define(
    'elmt-container',
    class extends HTMLElement {
      observeMutations() {
        const callback = (mutationList, observer) => {
          for (const mutation of mutationList) {
            if (mutation.type === 'childList') {
              if (mutation.addedNodes) {
                if (mutation.addedNodes[0] instanceof HTMLElement) {
                  console.log('A child node has been added:')
                  console.log(mutation.addedNodes[0])
                }
              }
            }
          }
        }
        const observer = new MutationObserver(callback)
        this.observer = observer
        const config = { childList: true, subtree: true }
        observer.observe(this, config)
      }
      disconnectedCallback() {
        const observer = this.observer
        observer.disconnect()
      }
      constructor() {
        super()
        this.observeMutations()
      }
    }
  )
  window.customElements.define('elmt-test', class extends HTMLElement {})
</script>

<elmt-container>
  <div>1</div>
  <elmt-test>2</elmt-test>
  <div><span>3</span></div>
</elmt-container>

Solution to this problem is quite simple. Just wait until the HTML is parsed, before calling customElements.register. The scheme I normally use when the HTML children have to be processed in connectedCallback:

class MyComponent extends HTMLElement{
  static {
     globalThis.addEventListener('load', ()=>{customElements.define('my-ponent', MyComponent)});
  }
}

本文标签: htmlwhen can we access the children elements of a custom component using javascriptStack Overflow