How to listen for value changes from class property TypeScript - Angular

72,757

Solution 1

You can still check component's field members value change by KeyValueDiffers via DoCheck lifehook.

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

differ: KeyValueDiffer<string, any>;
constructor(private differs: KeyValueDiffers) {
  this.differ = this.differs.find({}).create();
}

ngDoCheck() {
  const change = this.differ.diff(this);
  if (change) {
    change.forEachChangedItem(item => {
      console.log('item changed', item);
    });
  }
}

see demo.

Solution 2

I think the nicest solution to your issue is to use a decorator that replaces the original field with a property automatically, then on the setter you can create a SimpleChanges object similar to the one created by angular in order to use the same notification callback as for angular (alternatively you could create a different interface for these notifications, but the same principle applies)

import { OnChanges, SimpleChanges, DoCheck, SimpleChange } from '@angular/core';

function Watch() : PropertyDecorator & MethodDecorator{
    function isOnChanges(val: OnChanges): val is OnChanges{
        return !!(val as OnChanges).ngOnChanges
    }
    return (target : any, key: string | symbol, propDesc?: PropertyDescriptor) => {
        let privateKey = "_" + key.toString();
        let isNotFirstChangePrivateKey = "_" + key.toString() + 'IsNotFirstChange';
        propDesc = propDesc || {
            configurable: true,
            enumerable: true,
        };
        propDesc.get = propDesc.get || (function (this: any) { return this[privateKey] });

        const originalSetter = propDesc.set || (function (this: any, val: any) { this[privateKey] = val });

        propDesc.set = function (this: any, val: any) {
            let oldValue = this[key];
            if(val != oldValue) {
                originalSetter.call(this, val);
                let isNotFirstChange = this[isNotFirstChangePrivateKey];
                this[isNotFirstChangePrivateKey] = true;
                if(isOnChanges(this)) {
                    var changes: SimpleChanges = {
                        [key]: new SimpleChange(oldValue, val, !isNotFirstChange)
                    }
                    this.ngOnChanges(changes);
                }
            }
        }
        return propDesc;
    }
}

// Usage
export class MyClass implements OnChanges {


    //Properties what I want to track !
    @Watch()
    myProperty_1: boolean  =  true
    @Watch()
    myProperty_2 =  ['A', 'B', 'C'];
    @Watch()
    myProperty_3 = {};

    constructor() { }
    ngOnChanges(changes: SimpleChanges) {
        console.log(changes);
    }
}

var myInatsnce = new MyClass(); // outputs original field setting with firstChange == true
myInatsnce.myProperty_2 = ["F"]; // will be notified on subsequent changes with firstChange == false

Solution 3

as said you can use

public set myProperty_2(value: type): void {
 if(value) {
  //doMyCheck
 }

 this._myProperty_2 = value;
}

and then if you need to retrieve it

public get myProperty_2(): type {
  return this._myProperty_2;
}

in that way you can do all the checks that you want while setting/ getting your variables such this methods will fire every time you set/get the myProperty_2 property.

small demo: https://stackblitz.com/edit/angular-n72qlu

Solution 4

I think I came into the way to listen to DOM changes that you can get any changes that do to your element, I really hope these hints and tips will help you to fix your problem, following the following simple step:

First, you need to reference your element like this:

in HTML:

<section id="homepage-elements" #someElement>
....
</section>

And in your TS file of that component:

@ViewChild('someElement')
public someElement: ElementRef;

Second, you need to create an observer to listen to that element changes, you need to make your component ts file to implements AfterViewInit, OnDestroy, then implement that ngAfterViewInit() there (OnDestroy has a job later):

private changes: MutationObserver;
ngAfterViewInit(): void {
  console.debug(this.someElement.nativeElement);

  // This is just to demo 
  setInterval(() => {
    // Note: Renderer2 service you to inject with constructor, but this is just for demo so it is not really part of the answer
    this.renderer.setAttribute(this.someElement.nativeElement, 'my_custom', 'secondNow_' + (new Date().getSeconds()));
  }, 5000);

  // Here is the Mutation Observer for that element works
  this.changes = new MutationObserver((mutations: MutationRecord[]) => {
      mutations.forEach((mutation: MutationRecord) => {
        console.debug('Mutation record fired', mutation);
        console.debug(`Attribute '${mutation.attributeName}' changed to value `, mutation.target.attributes[mutation.attributeName].value);
      });
    }
  );

  // Here we start observer to work with that element
  this.changes.observe(this.someElement.nativeElement, {
    attributes: true,
    childList: true,
    characterData: true
  });
}

You will see the console will work with any changes on that element:

enter image description here

This is another example here that you will see 2 mutation records fired and for the class that changed:

// This is just to demo
setTimeout(() => {
   // Note: Renderer2 service you to inject with constructor, but this is just for demo so it is not really part of the answer
  this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds()));
  this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds() + 1));
}, 5000);

// Here is the Mutation Observer for that element works
this.changes = new MutationObserver((mutations: MutationRecord[]) => {
    mutations.forEach((mutation: MutationRecord) => {
      console.debug('Mutation record fired', mutation);
      if (mutation.attributeName == 'class') {
        console.debug(`Class changed, current class list`, mutation.target.classList);
      }
    });
  }
);

Console log:

enter image description here

And just housekeeping stuff, OnDestroy:

ngOnDestroy(): void {
  this.changes.disconnect();
}

Finally, you can look into this Reference: Listening to DOM Changes Using MutationObserver in Angular

Share:
72,757
L Y E S  -  C H I O U K H
Author by

L Y E S - C H I O U K H

Husband • Father • SoftwareEngineer • Full-stack • #Java &amp; #JavaScript enthousiast • #Angular • #RESTAPIs • #Algorithm • #Cloud

Updated on July 29, 2020

Comments

  • L Y E S  -  C H I O U K H
    L Y E S - C H I O U K H over 3 years

    In AngularJS, we can listen variable change using $watch, $digest... which is no longer possible with the new versions of Angular (5, 6).

    In Angular, this behaviour is now part of the component lifecycle.

    I checked on the official documention, articles and especially on Angular 5 change detection on mutable objects, to find out how to listen to a variable (class property) change in a TypeScript class / Angular

    What is proposed today is :

    import { OnChanges, SimpleChanges, DoCheck } from '@angular/core';
    
    
    @Component({
      selector: 'my-comp',
      templateUrl: 'my-comp.html',
      styleUrls: ['my-comp.css'],
      inputs:['input1', 'input2']
    })
    export class MyClass implements OnChanges, DoCheck, OnInit{
    
      //I can track changes for this properties
      @Input() input1:string;
      @Input() input2:string;
      
      //Properties what I want to track !
      myProperty_1: boolean
      myProperty_2: ['A', 'B', 'C'];
      myProperty_3: MysObject;
    
      constructor() { }
    
      ngOnInit() { }
    
      //Solution 1 - fired when Angular detects changes to the @Input properties
      ngOnChanges(changes: SimpleChanges) {
        //Action for change
      }
    
      //Solution 2 - Where Angular fails to detect the changes to the input property
      //the DoCheck allows us to implement our custom change detection
      ngDoCheck() {
        //Action for change
      }
    }

    This is only true for @Input() property !

    If I want to track changes of my component's own properties (myProperty_1, myProperty_2 or myProperty_3), this will not work.

    Can someone help me to solve this problematic ? Preferably a solution that is compatible with Angular 5