Angular 4: How to watch an object for changes?

Viewed 95369

ETA: I know that there are various ways to watch my form for changes. That is not what I am trying to do. As the title says, I am asking how to watch for changes to an object. The app shown below is for illustration purposes only. Please answer the question that I have asked. Thanks!

I have this simple app:

import { Component, OnInit } from '@angular/core';

export class Customer {
    firstName: string;
    favoriteColor: string;
}

@Component({
    selector: 'my-app',
    template: `
        <div *ngIf="customer">
            <input type="text" [(ngModel)]="customer.firstName">
            <input type="text" [(ngModel)]="customer.favoriteColor">
        </div>
        `
})
export class AppComponent implements OnInit {

    private customer: Customer;

    ngOnInit(): void {

        this.customer = new Customer();

        // TODO: how can I register a callback that will run whenever
        // any property of this.customer has been changed?

    }

}

Note the TODO. I need to register a callback that will run whenever any property of this.customer has been changed.

I cannot use ngChange on the inputs. I need to subscribe directly to changes on the model. The reasons pertain to my use case, and aren't worth going into here. Just trust me that this isn't an option.

Is this possible? I've done a lot of Googling, but I've come up dry.

6 Answers

Angular usually uses injected into constructor KeyValueDiffers class.

For your case it could look like:

import { KeyValueChanges, KeyValueDiffer, KeyValueDiffers } from '@angular/core';

export class Customer {
  firstName: string;
  favoriteColor: string;
}

@Component({
  selector: 'my-app',
  templateUrl: `./app.component.html`
})
export class AppComponent {
  private customerDiffer: KeyValueDiffer<string, any>;
  private customer: Customer;

  constructor(private differs: KeyValueDiffers) {}

  ngOnInit(): void {
    this.customer = new Customer();
    this.customerDiffer = this.differs.find(this.customer).create();
  }

  customerChanged(changes: KeyValueChanges<string, any>) {
    console.log('changes');
    /* If you want to see details then use
      changes.forEachRemovedItem((record) => ...);
      changes.forEachAddedItem((record) => ...);
      changes.forEachChangedItem((record) => ...);
    */
  }

  ngDoCheck(): void {
      const changes = this.customerDiffer.diff(this.customer);
      if (changes) {
        this.customerChanged(changes);
      }
  }
}

Stackblitz Example

One more option is using setter on properties that you want to check.

See also

I need to subscribe directly to changes on the model.

Then you need to listen to model changes with ngModelChange

Template:

<input type="text" (ngModelChange)="doSomething($event)" [ngModel]="customer.firstName">

Class:

doSomething(event) {
  console.log(event); // logs model value
}

DEMO

You can't watch changes in an object. Its not angular 1 there are no watchers here. Another solution will be via observables.

use form

<form #f="ngForm">
  <input type="text" name="firstName" [(ngModel)]="customer.firstName">
  <input type="text" name="favoriteColor" [(ngModel)]="customer.favoriteColor">
</form>

in code

@ViewChild('f') f;

ngAfterViewInit() {
  this.f.form.valueChanges.subscribe((change) => {
   console.log(change)
  })
}

You could use custom setters to trigger your callback:

class Customer {
  private _firstName: string
  get firstName(): string {
    return this._firstName
  }
  set firstName(firstName: string) {
    this.valueChanged(this._firstName, firstName)
    this._firstName = firstName
  }

  private _lastName: string
  get lastName(): string {
    return this._lastName
  }
  set lastName(lastName: string) {
    this.valueChanged(this._lastName, lastName)
    this._lastName = lastName
  }

  valueChanged: (oldVal, newVal) => void

  constructor (valueChanged?: (oldVal, newVal) => void) {
    // return an empty function if no callback was provided in case you don't need
    // one or want to assign it later
    this.valueChanged = valueChanged || (() => {})
  }
}

Then just assign the callback when you create the object:

this.customer = new Customer((oldVal, newVal) => console.log(oldVal, newVal))

// or

this.customer = new Customer()
this.customer.valueChanged = (oldVal, newVal) => console.log(oldVal, newVal)

visit https://github.com/cartant/rxjs-observe. it bases on rxjs and proxy.

import { observe } from "rxjs-observe";

const instance = { name: "Alice" };
const { observables, proxy } = observe(instance);
observables.name.subscribe(value => console.log(name));
proxy.name = "Bob";

We are tasked with converting an Angular 1.x app to Angular 9. It's an application with ESRI maps, so we have some neat tools that the ESRI framework brought to the table. ESRI has watchUtils that do a whole lot more than just watch for changes.

But I missed Angular 1's simple $watch. Besides, we create Entities and Models in our application, and we may need to observe these from time to time.

I created an abstract class called MappedPropertyClass. It uses a Map<string, any> to map class properties, which allows me to easily implement toJSON and other utility functions.

The other Map this class has is _propertyChangeMap: Map<string, EventEmitter<{newvalue,oldvalue}>;

We also have a function called... $watch, which takes a string and a callback function.

This class can be extended by Entities as well as components or services

I'm happy to share, the caveat is your properties must look like this:

public get foo(): string {
    return this._get("foo");
}  
public set foo(value:string) {
    this._set("foo", value);
}


--------------------------------------------------------------------
import { EventEmitter } from '@angular/core';

export abstract class MappedPropertyClass {
    private _properties: Map<string, any>;
    private _propertyChangeMap: Map<string, EventEmitter<{ newvalue, oldvalue }>>;

    protected _set(propertyName: string, propertyValue: any) {
        let oldValue = this._get(propertyName);
        this._properties.set(propertyName, propertyValue);
        this.getPropertyChangeEmitter(propertyName).emit({ newvalue: 
    propertyValue, oldvalue: oldValue });
    }

    protected _get(propertyName: string): any {
        if (!this._properties.has(propertyName)) {
            this._properties.set(propertyName, undefined);
        } 
        return this._properties.get(propertyName);
    }

    protected get properties(): Map<string, any> {
        var props = new Map<string, any>();
        for (let key of this._properties.keys()) {
            props.set(key, this._properties.get(key));
        }

        return props;
    }

    protected constructor() {
        this._properties = new Map<string, any>();
        this._propertyChangeMap = new Map<string, EventEmitter<{ newvalue: any, 
        oldvalue: any }>>();
    }

    private getPropertyChangeEmitter(propertyName: string): EventEmitter<{ 
                         newvalue, oldvalue }> {
        if (!this._propertyChangeMap.has(propertyName)) {
            this._propertyChangeMap.set(propertyName, new EventEmitter<{ newvalue, 
            oldvalue }>());
        }
        return this._propertyChangeMap.get(propertyName);
    }

    public $watch(propertyName: string, callback: (newvalue, oldvalue) => void): 
    any {
        return this.getPropertyChangeEmitter(propertyName).subscribe((results) => 
        {
            callback(results.newvalue, results.oldvalue);
        });
    }
}
Related