Search code examples
javascriptweb-componentnative-web-component

Binding the value property of an input to an attribute


I try to create my own custom DOM elements with Web Components (with validation and server communication). I am following the tutorial on the MDN: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements

    attributeChangedCallback(name, oldValue, newValue) {
    console.log(newValue);
  }

I am able to receive the change of an attribute. But if I have for example a textbox, where the value gets changed. How do I bind the value of the textbox to the attribute? Is this even a good way to do it?

This is my code:

'use strict';

class PopUpInfo extends HTMLElement {

    static get observedAttributes() {
        return ['value'];
    }
    constructor() {
      // Always call super first in constructor
      super();

      // write element functionality in here

    var shadow = this.attachShadow({mode: 'closed'});
    console.log("Created");
    let tbName = document.createElement("input");
    shadow.appendChild(tbName);
    }
    attributeChangedCallback(name, oldValue, newValue) {
        console.log(newValue);
      }
  }
  customElements.define('popup-info', PopUpInfo);

Later on, I would like to add multiple html controls to "PopUpInfo". The name would be later somthing like "Controlunit".


Solution

  • You need to take the attribute, or a property and pass that value into the inner DOM. There is no "binding" unless you are using a framework. If you want to use LitElement or other things then you get binding. Personally I see those frameworks as a huge amount of overhead.

    But look at this example:

    class PopUpInfo extends HTMLElement {
      static get observedAttributes() {
        return ['value'];
      }
      constructor() {
        // Always call super first in constructor
        super();
    
        // write element functionality in here
    
        var shadow = this.attachShadow({mode: 'open'});
        let textEl = document.createElement("input");
        shadow.appendChild(textEl);
        
        // Set attribute to value of inner element `.value`
        textEl.addEventListener('input', (evt) => {
          this.setAttribute('value', textEl.value);
        });
      }
      
      attributeChangedCallback(name, oldValue, newValue) {
        console.log(`attributeChangedCallback(${name}, ${oldValue}, ${newValue})`);
        if (oldValue !== newValue) {
          this.value = newValue;
        }
      }
      
      get value() {
        let textEl = this.shadowRoot.querySelector("input");
        return textEl.value;
      }
            
      set value(newValue) {
        console.log(`set value(${newValue})`);
        let textEl = this.shadowRoot.querySelector("input");
        if (newValue == null) { // check for null and undefined           textEl.value = '';
        }
        else {
          textEl.value = newValue;
        }
      }
    }
    
    customElements.define('popup-info', PopUpInfo);
    <popup-info value="my info"></popup-info>

    First: Since you are only watching one attribute your attributeChangedCallback function only needs to see if oldValue and newValue are different. If they are not different then there is nothing to do. If they are different they you use newValue.

    In my example I am passing the attribute's value on to the property called value.

    In the property setter I check to see if the value is null or undefined (Using double equals for null (x == null) does just that. If it is null or undefined then we set value of the inner <input> element to an empty string. If it is not null or undefined then we set value of the inner <input> element to whatever was sent in.

    The property getter just reads the value of the inner <input> element and returns that.