Angular @Input getter/setter and non-primitive val

2019-04-29 03:08发布

问题:

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

mock-data.service.ts

@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.ts (parent cmp)

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;
    }
  }
}

child-cmp.component.ts

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;
  }
}

回答1:

There are two parts to this:

  1. Recognizing that the @Input decorator is only updated during change detection, therefore the setter assigned to the bound data will only fire during change detection. This fact is clearly stated in the first two comment lines in the Angular source code.

export interface InputDecorator { /** * Declares a data-bound input property. * * Angular automatically updates data-bound properties during change detection. *

  1. From 1, it then follows that we need to understand how Angulars change detection applies to non-primatives.

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.