textContent empty in connectedCallback() of a custom HTMLElement

后端 未结 3 1328
说谎
说谎 2021-01-14 04:24

Within the connectedCallback() method of my custom element the textContent is returned as an empty string.

Essentially my code boils down t

相关标签:
3条回答
  • 2021-01-14 04:54

    You can access the content using a slot and the slotchange event (the slot gets the host tag content.)

    (function(){
        
        class MyComponent extends HTMLElement {
            
            constructor() {
                super();
                
                let slot = document.createElement('slot') ;
    
                slot.addEventListener('slotchange', function(e) {
                    let nodes = slot.assignedNodes();
                    console.log('host text: ',nodes[0].nodeValue);                               
                });
      
                const shadowRoot = this.attachShadow({mode: 'open'});
                shadowRoot.appendChild(slot);     
            }
            
        }
                
        window.customElements.define('my-component', MyComponent);
    })();
    <my-component>This is the content I need to access</my-component>

    0 讨论(0)
  • 2021-01-14 04:56

    The issue you're facing is essentially the same our team has run into in our current project:

    connectedCallback in Chrome does not guarantee children are parsed. Specifically, relying on children works in the upgrade case, but does not work if the element is known upfront when the browser parses it. So if you place your webcomponents.js bundle at the end of the body, it at least reliably works for the static document you have up until then (but will still fail if you create the element programmatically after DOMContentLoaded using document.write(which you shouldn't anyway)). This is basically what you have posted as your solution.

    To make matters worse, there is no lifecycle hook that does guarantee child element access in Custom Elements spec v1.

    So if your custom element relies on children to setup (and a simple textNode like your textContent is a child node), this is what we were able to extract after a week of excessive research and testing (which is what the Google AMP team does as well):

    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)
    <my-component>textNode here</my-component>

    Update: Already quite a while ago Andrea Giammarchi (@webreflection), the author of the custom elements polyfill document-register-element (which e.g. is being used by Google AMP), who is a strong advocate of introducing such a parsedCallback to the custom elements' API, has taken the above code and create a package html-parsed-element from it which might help you:

    https://github.com/WebReflection/html-parsed-element

    You simply derive your elements from the HTMLParsedElement base class that package provides (instead of HTMLElement). That base class, in turn, inherits from HTMLElement.

    0 讨论(0)
  • 2021-01-14 05:03

    I managed to work around this by only calling customElements.define('my-component', MyComponent); after the DOMContentLoaded event had fired.

    document.addEventListener('DOMContentLoaded', function() {
        customElements.define('my-component', MyComponent);   
    }
    

    This behaviour seems a little odd as you'd expect that connectedCallback would only fire once the node has been inserted into the DOM and is fully ready to be manipulated.

    0 讨论(0)
提交回复
热议问题