How to detect data property on web component set or altered - javascript

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..

Related

Vuejs composition API - property changes

I'm curious about passing props into setup and what are best practices to update variables/templates based on property changes.
I'm curious about reactive and computed.
For example:
setup(props) {
// Setup global config settings
const config = computed(() => {
return {
// See if the component is disabled
isDisabled: props.disabled, // (1)
// Test for rounded
isRounded: props.rounded // (2)
}
})
return { config }
}
Should config.isDisabled and config.isRounded be wrapped in their own computed function as they are both different and independent? However, it is easy to just stick them into one big function. What is best practice in this regard?
Does the entire config function evaluate once a single property changes within the function or can it recognize the change and update what is required?
Per docs, reactive is deeply reactive and used for objects, however, I've noticed it doesn't update to property changes. Therefore, I've been treating it more like data in Vue 2. Am I missing something or is this correct treatment?
You do not have to wrap props with computed at all, as they should be already reactive and immutable.
You also do not have to return config from your setup function as all props passed to your component should be automatically exposed to your template.
The computed function is evaluated only once and then Vue3 uses Proxy to observe changes to values and update only what's required. If you need to run a function every time a property changes you can use watchEffect.
Vue3 reactive is actually deep and works fine on objects. It should track all changes, unless you are trying to change the original object (the target of reactive function).

Angular (9) sharing common state between components

In current (2020) Angular, I have two components that are intended to share the state of activeProject through a service. I have the following defined on an ApplicationProjectService:
private activeProjectSource = new BehaviorSubject(undefined);
activeProject$ = this.activeProjectSource.asObservable();
set activeProject(v: any) {
this.activeProjectSource.next(v);
}
get activeProject() {
return this.activeProjectSource.value;
}
I'm using BehaviorSubject in the service since I want components to get the current value upon subscribing without any change. The getter/setter is there because I was doing some other binding directly to a service property, which I've since learned is not recommended.
The two sibling components that eventually trace back to a common parent, but I'm not using #Input() or #Output() or any parameter passing in the DOM:
this.appProjectService.activeProject$.subscribe(activeProject => {
this.activeProject = activeProject;
});
Each component is binding to the this.activeProject property in their respective component using [(ngModel)]:
<input type="checkbox" [(ngModel)]="activeProject.someProperty">
Question
If each component obtained what I thought was a copy of activeProject through this.appProjectService.activeProject$.subscribe(), how is it working that a change to the local property in one component is reflected in the other? In the end this is the behavior I want, but I can't understand why it works. Is there some passing by reference that I'm not understanding in rxjs observables?
sIf you have 2 components, the both local variables activeProject use the same reference of activeProject. ngModel is bound to a property of this reference. So it's working, because a change in a component only update the property of the reference, and does not change the reference. You can even use a variable activeProject without wrapping it in a BehaviorSubject.
I know this should be in comment but this much of letters comment won't accept.
Forget about RxJS for a while.
Now you have getter and setter for your property.
You set activeProjectValue in your service.
Now when you subscribe it in one component, you will get the object which will be passed by reference. Same for the other component. As both components accessing same object they are passed by reference.
If you have to break the reference, to use it differently.
Also each component obtained what I thought was a copy of activeProject .... this means they copy by refenrence of object.
I know, you know how to break reference, but this is just for sake for future viewers
To break the reference of object you can use JSON.parse(JSON.stringify(*ObjectName*)
In your example
this.appProjectService.activeProject$.subscribe(activeProject => {
this.activeProject = JSON.parse(JSON.stringify(activeProject));
});

Web Components: setter not being called

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.

Can a prop be reactive?

I am writing a component which receives data via a prop, modifies that data, and then $emit it back to the parent (upon its change, so it is also in watched).
Can a prop be reactive? Or, alternatively, how to assign a prop to a reactive property?
I tried to avoid this question by:
having props: ["receiveddata"]
defining a reactive property in data (say, hello: '')
in mounted() ensure that this.hello = this.receiveddata
I expected at that point this.hello to carry the content brought in by the receiveddata prop, and from that point on work on this.hello (that is: modify it, watch it and $emit it when relevant)
However, when looking at the values in DevTools I see that
this.hello is undefined
this.receiveddata correctly holds the data passed to the component
I conclude that, when in mounted(), the props values are not yet known (thus the undefined of this.receiveddata), this undefined is assigned to this.hello, and then later this.receiveddata gets populated (but it's too late for me).
In other words, I plugged my this.hello = this.receiveddata in the wrong place, but I do not really know where it could go elsewhere (I also tried to change mounted() to created())
I might not be understanding 100% but I think this is what you're after.
I typically use a computed property with a getter and a setter in this case.
props: {
someProp: String,
},
computed: {
somePropLocal: {
get () {
return this.someProp
},
set (value) {
this.$emit('input', value)
},
},
}
This way you can do this freely and the proper value will be emitted.
this.somePropLocal = 'Whatever'
Also, somePropLocal will be updated if the parent changes it directly as well.

Force dirty checking in Aurelia

I have an array of objects that I am binding to the DOM via a ValueConverter. Aurelia can't quite figure out what I'm doing in the ValueConverter, and so it isn't updating properly. I want to force dirty checking on this object. How can I do that?
Expose your array via a property getter.
Instead of:
export class Foo {
myArray = []; // will be observed without dirty-checking
}
Use a property getter:
export class Foo {
_myArray = []; // internal value that you will manipulate as-needed
get myArray() { // this property will be dirty-checked.
return this._myArray;
}
}

Categories