Web Components: setter not being called

Viewed 2682

Say I have a Web Component:

customElements.define("custom-list", class CustomList extends HTMLElement {
  get filter() {
    console.log("get filter");
    return this.getAttribute("filter");
  }

  set filter(value) {
    console.log("set filter");
    this.setAttribute("filter", value);
  }
});

I wanted to use the setter method to do some initial attribute validation, but the setter never gets called. I tried setting the attribute through the HTML:

<custom-list filter="some value"></custom-list>

Only when I use JavaScript to set a property programmatically does the setter get called:

var list = document.querySelector("custom-list");
list.filter = "some value";

list.setAttribute("filter", "some value"); // DOESN'T WORK EITHER

So, it seems like setting attributes through the HTML or using setAttribute doesn't trigger the setter, which I partly can understand. My questions are:

  • Is the setter only necessary when I want to set properties programmatically?
  • How could I do initial validation of an attribute? In the connectedCallback? Say I want to only accept a certain string, how would I detect that?
  • Since the property filter gets populated anyway, do I need the setter if I don't use JavaScript to set my attributes?
2 Answers
  • Is the setter only necessary when I want to set properties programmatically?

Yes, at least if you want/need to run some tests/filtering upon the value you want to set.

  • How could I do initial validation of an attribute? In the connectedCallback? Say I want to only accept a certain string, how would I detect that?

Yep, connectedCallback or even in the constructor.

  • Since the property filter gets populated anyway, do I need the setter if I don't use JavaScript to set my attributes ?

No, you don't

This being said if you need a clear control over your custom attributes, i would suggest creating an internal state that you populate once when your custom element is being created and then when attributeChangedCallback is being called. That would give you some advantages :

  • you get control over the values that value your custom attributes.
  • you get an internal state that you can use to re-render your component if you need to

Here is an example :

customElements.define("custom-list", class CustomList extends HTMLElement {

    static get observedAttributes() { return ['filter']; }

    constructor() {
        super();
        this.state = {
            filter: null
        };
        this.setFilter(this.getAttribute("filter"));
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "filter") {
            this.setFilter(newValue);
        }
    }

    getFilter() {
        console.log("get filter");
        return this.state.filter;
    }

    setFilter(value) {
        // You can add some logic here to control the value
        console.log("set filter");
        this.state.filter=value;
    }
});

Then you can call the following to change your internal state :

list.setAttribute("filter", "some value");

Would be interrested to get some feedback on this from the community. Anyway, hope this helps :)

getters and setters allow your code to receive values other than strings. Attributes are always strings and can only be called by JavaScript.

You can emulate a non-string in an attribute by parsing the value. But they are always passed in as strings.

If you want to have code run when an attribute is changed then you need to add the attributeChangedCallback function and indicate which attributes you are watching in the observedAttributes static getter. Attributes can be set in JavaScript by calling setAttribute and removeAttribute. They are also set when the browser parses your HTML due to a page load or setting innerHTML. But even then the browser eventually calls setAttribute in the background.

customElements.define("custom-list", class CustomList extends HTMLElement {
  static get observedAttributes() { return ['filter']; }

  constructor() {
    super();
    this._filter = null;
  }

  attributeChangedCallback(attr, oldVal, newVal) {
    if (oldVal != newVal) {
      // Only set this value if it is different
      this.filter = newVal;
    }
  }

  get filter() {
    console.log("get filter");
    return this._filter;
  }

  set filter(value) {
    if (value !== this._filter) {
      console.log(`set filter ${value}`);
      this._filter=value;
      this.textContent = value;
      // If you want the filter property to always show
      // in the attributes then do this:
      if (value !== null) {
        this.setAttribute('filter', value);
      } else {
        this.removeAttribute('filter');
      }
    }
  }
});

const el = document.querySelector('custom-list');
setTimeout(() => {
  el.filter = 'happy';
}, 2000);
<custom-list filter="10"></custom-list>

Always check to see if your oldVal and newVal are different in the function attributeChangedCallback.

It is also recommended that you check for different values in your setters.

Setters also allow you to take specific data types. For example you could check to see if the value for value was a number and, if not, throw a TypeError.

Setters also allow you to make sure a value is valid. Maybe it must be a positive number or one of three possible strings. If it isn't you can throw a RangeError.

But you have to remember that attributes are always strings. And properties can be anything.

Related