The Problem: I want be able to call a function each time a property in the object the child component is bound to changes. However, the setter is only called once, even though the bound input property can visibly be seen updating.
This all came to be from the need to have a child component bind to its parent components property that happens to be a complex object with deeply nested properties. I've learned that the Angular onChange event does not fire when a nested property in an object changes. Hence the decision to use getters/setters instead. However, as seen by this question using getters/setters did not work either. I've since changed my child component to subscribe to the same Observable that the parent component is subscribed to, thereby receiving the updates directly from the service and bypassing the parent component all together. I've done a lot of research on Angulars binding and TypeScript getters/setters and by all accounts it looks like my code show work, but it does not.
Goal: Understand why binding to a parent components property in a child component by using @Input with a getter/setter does not work as expected for non-primative types. Is there a fundamental concept I am missing or is there an implementation error in my code?
I will show some source code here and also attach a StackBlitz for anyone who wants to see it live in action. StackBlitz Live Demo
@Injectable()
export class MockDataService {
public updateSubject: Subject<any> = new Subject();
public numObj = {
'prop1': 'stuff',
'prop2': 'stuff',
'prop3': 'stuff',
'prop4': 'stuff',
'level1': {
'level2': {
'target': 0 //target is the prop that will be getting updated
}
}
}
constructor() {
this.startDemo();
}
private startDemo(): void {
//This is simulating the server sending updates
//to the numObj
setInterval(() => {
this.numObj.level1.level2.target += 1;
this.update();
}, 4000);
}
private update(): void {
try {
this.updateSubject.next(this.numObj);
} catch (err) {
this.updateSubject.error(err);
}
}
}
app.component.html
<child-cmp [targetNumber]="targetNumber"></child-cmp>
export class AppComponent implements OnInit {
public targetNumber: any;
public displayCurrentNumber: number;
constructor(private mockDataService: MockDataService){}
ngOnInit(){
this.mockDataService.updateSubject.subscribe({
next:(data) => this.onUpdate(data),
error: (error) => alert(error),
});
}
private onUpdate(data: any): void{
if(data){
this.targetNumber = data;
this.displayCurrentNumber = data.level1.level2.target;
}
}
}
export class ChildCmpComponent {
private _targetNum: any;
public displayNumberObj: any;
public displayNumber: number;
public changeArray: string[] = [];
@Input()
set targetNumber(target: any){
this.changeArray.push('Setter(),');
this._targetNum = target;
this.setDisplay(this._targetNum);
}
get targetNumber(): any{
this.changeArray.push('Getter(),');
return this._targetNum;
}
private setDisplay(target: any): void{
this.changeArray.push('setDisplay(),');
this.displayNumberObj = target;
this.displayNumber = target.level1.level2.target;
}
}
There are two parts to this:
export interface InputDecorator {
/**
* Declares a data-bound input property.
*
* Angular automatically updates data-bound properties during change detection.
*
To help explain this I will use the following object ObjA:
public ObjA = {
'prop1': 'value1',
'prop2': 'value2'
}
Angulars change detection fires when the value of the data bound property changes. However, when the property being bound to is an object like ObjA
, it is a reference of ObjA
that gets bound to, not the object itself. It is for this reason when a property value in ObjA
changes ( a state change) Angulars change detection does not fire. Angular is not aware of the state of ObjA
, but rather the reference to ObjA
.
Thank you to @JBNizet and @Jota.Toledo for providing me the information (in the above comments) I needed to understand this topic.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With