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?
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
I have built a component that composed of an array(selectedItems) and a boolean (toggle).
the component has items array.
based on the current data both hold I created a computed property:
computed:{
isValid(){
const {toggle, selectedItems} = this
return toggle && selectedItems.length || !toggle && selectedItems.length < items.length
}
}
my model is an object that has all 3: {selectedItems, isValid, toggle}
I wrote a function to update the model (which is named output):
updateModel(key, value) {
this.$emit('update', { ...this.output, [key]: value })
}
I'm calling this function when updating toggle or selectedItems, but I want it to be up to date with the validity property as well.
when I call that function in the computed it enters infinite loop of events for some reason, which I couldn't figure out.
Then I tried a different approach,
I created a watch on the computed isValid property and in the handler I call updateModel (the same way I tried to call it before, and that works.
Why calling it in the computed function causing it to some to be invoked again and again?
Thanks.
HTML:
<comp-two></comp-two>
JS:
class CompTwo extends HTMLElement {
constructor() {
super()
this._options=[]
}
get options() {
return this._options
}
set options(val) {
this._options = val
}
}
const el = document.querySelector('comp-two')
el.options = ['one','two','three']
Is there an accepted method for notifying the code inside the webComponent that a property has been set? If it makes any difference I'm nesting web components.
I can see that setting an attribute would do it, but seems wasteful?
A property can be read directly:
console.log(this.propertyName)
But as the value could change any time after the component is created, the problem is to know when a change occurs, similar to how attributeChangedCallback is used to act on attribute updates.
A setter will trigger when the named property is written to. Just like attributeChangedCallback for attributes, the property's value has not necessarily changed.
set propertyName(val){
this._propertyName = val // keep a copy, name must be different
// underscore is popular method
this.doSomething(val) // act on the new property value
}
get propertyName(){
return this._propertyName
}
Note: After including a setter the property can no longer be read directly. Use a getter to return the local copy saved by the setter, or just read the local copy directly.
The penny finally dropped for me..
I have a input component, and I want to set the default value when I open it (like set the display property block), and then I can control the input.
code example
I tried to use componentWillReceiveProps or the new lifecycle getDerivedStateFromProps. But I found some articles saying that this is antipattern.
Is there a better way to accomplish my goals?
getDerivedStateFromProps is a good choice, add code below to your InputForm:
static getDerivedStateFromProps(nextProps, state){
if(nextProps.show && !state.show) {
return {
show: true,
value: 'default value'
}
}
return null;
}
https://jsfiddle.net/dkhz32x9/
When my template renders the function inside ngOnChanges fires one time, and then only when an #input changes. Is this the expected behaviour and how can I prevent it?
Child:
export class MobileMenuComponent implements OnInit, OnChanges {
#Input('test') dezeTest: string;
//declare menu
menu;
constructor() {
//define menu
this.menu = {state: 'inactive'};
}
togglemenu() {
var that = this;
if (that.menu.state === 'inactive'){
that.menu.state = 'active';
}
else {
that.menu.state = 'inactive';
}
}
ngOnChanges(changes: SimpleChanges) {
this.togglemenu();
}
}
This is the normal behaviour of ngOnChanges.
The ngOnChanges method will fire the first time because your properties have been checked, and subsequently fire when a property is updated. From the documentation, you can see
ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed.
If you want to change it, you need to consider how you want to change it. That is not very clear from your question, but if you want to prevent the ngOnChanges from firing again, when a property is updated (I think that you want this because of your toggleMenu() you might consider using the ngOnInit() instead of ngOnChanges(). Alternatively, you can block the togglemenu(); after the first time.
firstrun : boolean = true; // class variable
ngOnChanges(changes : SimpleChanges){
if(firstrun){
this.togglemenu();
firstrun = false;
}
}
Alternatively, as hinted at earlier, another lifecycle hook might suit your needs better.
To expand on existing answers, and address a typing question raised in a comment at the same time:
The SimpleChange#firstChange field exists for this exact case.
Alternatively, because the value is set on your Component before ngOnChanges is called, you can also check if a field has changed, followed by if it's set:
ngOnChanges(changes: { myField: SimpleChange }) {
if(changes.myField && this.myField){
// do a thing only when 'myField' changes and is not nullish.
}
// Or, if you prefer:
if(changes.myField && changes.myField.firstChange){
// Do a thing only on the first change of 'myField'.
// WARNING! If you initialize the value within this class
// (e.g. in the constructor) you can get null values for your first change
}
}
Another little warning: If you were to use tools like WebStorm to rename 'myField' on your Component, the 'myField' of the ngOnChanges method parameters ({myField: SimpleChange }) will NOT be updated. Which can lead to some fun Component initialization errors.
As Dylan Meeus suggested, its the normal behaviour.
but i would suggest a different solution, which takes advantage of the passed SimpleChange object. it contains a previousValue and a currentValue.. initially, the previousValue is not set.
https://angular.io/docs/ts/latest/api/core/index/SimpleChange-class.html
ngOnChanges(changes : any){
if(changes.menu.previousValue){
this.togglemenu();
}
}
additionally, take care about OnChanges, since it fires on every input param... (you might add some more in the future)
class AbstractClass {
constructor() {
}
set property(value) {
this.property_ = value;
}
get property() {
return this.property_;
}
}
class Subclass extends AbstractClass {
constructor() {
super();
}
set property(value) {
super.property = value;
if (!(this.property_ instanceof SubclassAssociatedClass)) throw new TypeError();
}
//get property() {
// return super.property;
//}
}
Override the set method of an attribute and it appears the get method must be overridden also, otherwise undefined is returned (i.e., the get method is not inherited, uncomment the subclass get property() method above and everything works fine).
I assume this is a part of the spec., it would follow though possibly if the behaviour was a consequence of cross compiling. Just to be sure, is this the correct way to code overridden setters and getters (both at the same time or not at all)?
Yes, this is intentional (a part of the spec). If an object has an own property (.property in your example), this property will be used and not an inherited one. If that property is existent, but is an accessor property without a getter, then undefined will be returned.
Notice that this behaviour has not changed from ES5.