Search code examples
javascripthtmlecmascript-6web-componentcustom-tags

Defining multiple custom attributes on a custom HTML Element


Adding a getter/setter for a custom attribute on a custom HTML element is fairly straightforward:

  customElements.define('custom-el', class extends HTMLElement {
    
    static get observedAttributes() {
      return ['customAttr']
    }

    get customAttr() {
      return this.getAttribute('customAttr')
    }

    set customAttr(value) {
      this.setAttribute('customAttr', value)
    }

    constructor() {
      super();
    }
    
  })

However, this gets really cumbersome if you have multiple custom attributes on the same element:

  customElements.define('custom-el', class extends HTMLElement {
    
    static get observedAttributes() {
      return ['customAttr1', 'customAttr2', 'customAttr3']
    }

    // for attr 1

    get customAttr1() {
      return this.getAttribute('customAttr1')
    }

    set customAttr1(value) {
      this.setAttribute('customAttr1', value)
    }

    // for attr 2

    get customAttr2() {
      return this.getAttribute('customAttr2')
    }

    set customAttr2(value) {
      this.setAttribute('customAttr2', value)
    }

    // for attr 3

    get customAttr3() {
      return this.getAttribute('customAttr3')
    }

    set customAttr3(value) {
      this.setAttribute('customAttr3', value)
    }

    constructor() {
      super();
    }
    
  })

I've been thinking of ways one could write a function that would generate getter/setters from an array of custom attribute names, but that doesn't seem possible without being able to pass a variable string as the name of the get/set.

The documentation for "getter" suggests that expressions in square brackets are acceptable, and this works with one variable string:

const x = "customAttr";

customElements.define('custom-el', class extends HTMLElement {
    
  static get observedAttributes() {
    return x;
  }

  get [x]() {
    return this.getAttribute(x)
  }

  set [x](value) {
    this.setAttribute(x, value)
  }

  constructor() {
    super(); 
  }

})
<custom-el customAttr="It works"></custom-el>
console.log(document.querySelector("custom-el").customAttr) // yields "It works"

Unfortunately, for/forEach loops are not permissible in the body of a custom element definition (outside of an embedded subroutine, at which point the get/set operations no longer work!):

const customAttrs = ["customAttr1", "customAttr2", "customAttr3"];

customElements.define('custom-el', class extends HTMLElement {
    
  static get observedAttributes() {
    return customAttrs;
  }

  customAttrs.forEach( customAttr => {
    get [customAttr]() {
      return this.getAttribute(customAttr)
    }

    set [customAttr](value) {
      this.setAttribute(customAttr, value)
    }
  })

  constructor() {
    super(); 
  }

})
<custom-el customAttr1="It " customAttr2="doesn't " customAttr3="work"></custom-el>
let customEl = document.querySelector("custom-el");
console.log(testEl.customAttr1+testEl.customAttr2+testEl.customAttr3) // yields error

If the custom HTMLElement class had a "defineAttribute" method which operated like an Object's "defineProperty", you could do something like:

const customEl = ...

const customAttrs = ["customAttr1", "customAttr2", "customAttr3", ...]

customAttrs.forEach( customAttr => {
  HTMLElement.defineAttribute(customEl, customAttr, {
    get : function(){ return this.getAttribute(customAttr ); },
    set : function(newValue){ this.setAttribute(customAttr, newValue); }
  }
}

... but nothing like this seems to exist.

tl;dr - Is there really no better way to set up multiple setter/getters for custom attributes on custom elements than just writing set/get definitions out for each one?


Solution

  • use Object.defineProperty( element , attrName , { getter , setter , options} )

    Add something like this to your BaseClass (so this is your Custom Element)

      defineProperty(
        attr,
        getter = (attr) => this.getAttribute(attr),
        setter = (val, attr) => (val == undefined ? this.removeAttribute(attr) : this.setAttribute(attr, val)),
        options = {
          configurable: true,
        }
      ) {
        return Object.defineProperty(this, attr, {
          get() {
            return getter(attr);
          },
          set(val) {
            return setter(val, attr);
          },
          ...options,
        });
      }
    
    

    See:

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

    Plural:

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties