Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

custom elements not setting/getting attributes

if i create two custom elements and create one of them inside the other one, the attributes are not working on the new childs of the first element.

class Bar extends HTMLElement {
  constructor() {
    super();
    const val = this.getAttribute('val') || 'no value';

    const shadow = this.attachShadow({mode: 'open'});
    const wrapper = document.createElement('div');
    wrapper.innerHTML = `
      <div class='bar'>
      	<span>${val}</span>
      </div>
    `;

    shadow.appendChild(wrapper);
  }
}
customElements.define('x-bar', Bar);

class Foo extends HTMLElement {
  constructor() {
    super();
    const loop = this.getAttribute('loop') || 10;

    const shadow = this.attachShadow({mode: 'open'});
    const wrapper = document.createElement('div');
    
    for(let i=0; i<loop; i++){
    	const b = document.createElement('x-bar');
        b.setAttribute('val', `value #${i}`);
      
        wrapper.appendChild(b);
    }

    shadow.appendChild(wrapper);
  }
}
customElements.define('x-foo', Foo);
<x-foo loop='3'></x-foo>

im expecting that my output would be

value #0

value #1

value #2

as i have set the attr like this b.setAttribute('val', value #${i});

but im getting 3x no value

Any inputs on why that is? and/or how to fix it, Thanks!

like image 811
keja Avatar asked Jun 25 '26 23:06

keja


1 Answers

Most people are unaware of the rules of a constructor for Web Components:

Here are the official docs:

https://w3c.github.io/webcomponents/spec/custom/#custom-element-conformance

The summary is:

Your constructor code:

  • must have a parameter-less call to super() as first statement in constructor.
  • must not have a return statement anywhere in constructor.
  • must not call document.write() or document.open().
  • must not inspect element's attributes.
  • must not change or add any attributes or children.

In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.

In general I agree with @T.J. Crowder, but I would make one minor modification to the Bar object:

class Bar extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
  }
  
  static get observedAttributes() {
    // Indicate that we want to be notified
    // when the `val` attribute is changed
    return ['val'];
  }
  
  connectedCallback() {
    // Render the initial value
    // when this element is placed into the DOM
    render(this.getAttribute('val'));
  }
  
  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal != newVal) {
      // If the value for the `val` attribute has changed
      // then re-render this element
      render(newVal);
    }
  }  
  
  render(val = 'no value') {
    this.shadowRoot.innerHTML = `
      <div class='bar'>
      	<span>${val}</span>
      </div>
    `;
  }
}

customElements.define('x-bar', Bar);

This used the standard of attributeChangedCallback and observedAttributes. While overriding the setAttribute functionality works it is not future proof. If the API for setAttribute were to change, in the future, then you would need to remember to fix your component. And doing this with many components racks up a lot of developer debt.

class Bar extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    // Render the blank DOM
    this.shadowRoot.innerHTML = '<div class="bar"><span>no value</span><div>';
    this._span = this.shadowRoot.querySelector('span');
  }
  
  static get observedAttributes() {
    // Indicate that we want to be notified
    // when the `val` attribute is changed
    return ['val'];
  }
  
  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal != newVal) {
      // If the value for the `val` attribute has changed
      // then insert the value into the `<span>`
      this._span.textContent = newVal || 'no value';
      // OR: this._span.innerHTML = newVal || 'no value';
      // But make sure someone has not tried to hit you
      // with a script attack.
    }
  }
  
  get val() {
    return this._span.textContent;
  }
  set val(newVal) {
    if (newVal == null || newVal === false || newVal === '') {
      this.removeAttribute('val');
    }
    else {
      this.setAttribute('val', newVal);
    }
  }
}

customElements.define('x-bar', Bar);

This second method done not re-render the entire DOM it simple inserts the modified attribute value into the <span> tag.

It also provides properties so you can set the value through the attribute as well as through JavaScript:

var el = document.querySelector('x-bar');
if (el) {
  el.val = "A New String";
  setTimeout(()=>el.val = '';,2000);
}
like image 195
Intervalia Avatar answered Jun 28 '26 13:06

Intervalia



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!