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?
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
Reset to default 4getters and setters are a way to define properties that allow your code to receive and return values other than strings.
Attributes on the element are always string values. You can emulate a non-string in an attribute by parsing the value. But they are always set in as strings and read back 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 does not set these until after the constructor is called when 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; // Save the initial value
}
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal != newVal) {
// Only set this value if it is different
this.filter = newVal; // Call the setter
}
}
get filter() {
console.log("get filter");
return this._filter; // Return the internal value
}
set filter(value) {
// Only run this if the new value is different from the internal value
if (value !== this._filter) {
console.log(`set filter ${value}`);
this._filter = value; // Set the internal 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
. Otherwise you could end up in an endless loop.
It is also remended that you check for different values in your setters. Again, to avoid getting into an endless loop.
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
. Or you just convert the ining value to the correct type.
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.
- 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 ponent 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 munity. Anyway, hope this helps :)