Search code examples
javascripthtmlshadow-domcustom-element

Custom element not picking up attributes


I am trying to have a look at custom elements and how they work and while the examples on MDN work fine I'm seemingly unable to replicate them myself.

The MDN article is here.

This is a working example from MDN.

My problem is that I can't ever seem to pass attributes into my component, they always come out as null instead of passing over the value of the parameter.

My JS is (test.js)

  class PopUpInfo extends HTMLElement {
    constructor() {
      // Always call super first in constructor
      super();
  
      // Create a shadow root
      const shadow = this.attachShadow({mode: 'open'});
  
      // Create spans
      const wrapper = document.createElement('span');
      const info = document.createElement('span');
 
      // Take attribute content and put it inside the info span
      const text = this.getAttribute('foo'); // <-- this always returns null
      info.textContent = `(${text})`;

      shadow.appendChild(wrapper);
      wrapper.appendChild(info);
    }
  }
  
  // Define the new element
  customElements.define('popup-info', PopUpInfo);

And my Html:

<html>
    <head>
        <script src="test.js"></script>
    </head>
    <body>

        <hr>

        <popup-info foo="Hello World"></popup-info> 

        <hr>

    </body>
</html>

What I'm expecting to see on screen is the text

(Hello World)

but all I ever see is

(null)

When I debug I can see that this.attributes has a length of 0 so it's not being passed in.

Has anyone seen this before when creating custom elements?


Solution

  • Although your example seems to run fine when I try to run it here in a snippet, I still want to make a suggestion to improve it.

    Use the observedAttributes static getter to define a list of attributes which the component should keep an eye on. When the value of an attribute has been changed and the name of the attribute is in the list, then attributeChangedCallback callback is called. In there you can assert logic on what to do whenever you attribute value has been changed.

    In this case you could build your string that you desire. This also has the side effect that whenever the attribute value is changed again, the string will be updated.

    class PopUpInfo extends HTMLElement {
    
      /**
       * Observe the foo attribute for changes.
       */
      static get observedAttributes() {
        return ['foo'];
      }
    
      constructor() {
        super();
    
        const shadow = this.attachShadow({
          mode: 'open'
        });
    
        const wrapper = document.createElement('span');
        const info = document.createElement('span');
        wrapper.classList.add('wrapper');
    
        wrapper.appendChild(info);
        shadow.appendChild(wrapper);
      }
      
      /**
       * Returns the wrapper element from the shadowRoot.
       */
      get wrapper() {
        return this.shadowRoot.querySelector('.wrapper')
      }
      
      /**
       * Is called when observed attributes have a changed value.
       */
      attributeChangedCallback(attrName, oldValue, newValue) {
        switch(attrName) {
          case 'foo':
            this.wrapper.textContent = `(${newValue})`;
            break;
        }
      }
      
    }
    
    // Define the new element
    customElements.define('popup-info', PopUpInfo);
    <html>
    
    <head>
      <script src="test.js"></script>
    </head>
    
    <body>
    
      <hr>
    
      <popup-info foo="Hello World"></popup-info>
    
      <hr>
    
    </body>
    
    </html>