Angular KeyValueDiffers + Detect Changes in Objects

By Arvind Rai, February 13, 2024
This page will walk through Angular KeyValueDiffers to detect changes in objects. KeyValueDiffers is the repository of different map diffing strategies. Angular uses KeyValueDiffers internally for directives NgClass, NgStyle to detect any change. We can also use KeyValueDiffers to perform custom change-detection in our application. Angular provides ngDoCheck() lifecycle hook to detect custom changes. For any type of change, ngDoCheck() will be called and to identify the type of change and the object, we can use Angular KeyValueDiffers and IterableDiffers. Angular uses IterableDiffers in NgFor, NgClass directive internally to detect changes.
On this page we will create an example to detect changes in array of objects and changes in property value of objects inside array using KeyValueDiffers. Now find the complete example step-by-step.

ngDoCheck()

ngDoCheck() is a lifecycle hook that works as a custom change-detection function in addition to check performed by the default change-detector. ngDoCheck() is called during every change-detection run immediately after ngOnChanges() and ngOnInit() lifecycle hook. To use ngDoCheck(), our components need to implement DoCheck.
@Component({
  selector: 'app-emp',
  ------ 
})
export class EmployeeComponent implements DoCheck {
  ngDoCheck() {
    let empListChanges = this.listDiffer.diff(this.empArray);
    if (empListChanges) {
      console.log('... Array changes ...');
    });
  }
} 

KeyValueDiffers

KeyValueDiffers is the repository of different map diffing strategies. Angular uses it internally for directives NgClass, NgStyle etc. Whenever there is change in binding values of these directives, the changes reflects. To handle and update the changes, Angular uses KeyValueDiffers.
In the same way, Angular provides IterableDiffers that is the repository of different iterable diffing strategies used by directives NgFor, NgClass etc. Here on this page we will discuss using KeyValueDiffers with examples. In our example, we will detect changes in our user objects and notify. Find the steps to detect changes using KeyValueDiffers.
1. KeyValueDiffers can be injected into component using constructor.
constructor(private kvDiffers: KeyValueDiffers) {} 
2. KeyValueDiffers provides find method.
find(kv: any): KeyValueDifferFactory 
kv: Pass the object that needs to be detected for changes.
find method returns KeyValueDifferFactory.

3. KeyValueDifferFactory provides factory for KeyValueDiffer. To get KeyValueDiffer instance, KeyValueDifferFactory provides create method.
create<K, V>(): KeyValueDiffer<K, V> 
Find the sample code to get instance of KeyValueDiffer.
this.arrayDiffer = this.kvDiffers.find(this.empArray).create(); 
Here arrayDiffer is the instance of KeyValueDiffer.

4. KeyValueDiffer is a differ that tracks changes made to an object over time. It has a diff method to compute a difference between the previous state and the new object state.
diff(object: { [key: string]: V; }): KeyValueChanges<string, V> | null 
We need to pass the object containing the new value and it returns KeyValueChanges.
let empArrayChanges = this.arrayDiffer.diff(this.empArray); 
Here empArrayChanges is the instance of KeyValueChanges.

5. KeyValueChanges keeps the changes in the map since the last time the method diff was invoked. KeyValueChanges has following methods.
forEachItem: Iterates over all changes.
forEachPreviousItem: Iterates over previous items that has changed.
forEachChangedItem: Iterates those items whose value is changed.
forEachAddedItem: Iterates over all added items.
forEachRemovedItem: Iterates over all removed items.
All the above methods of KeyValueChanges provides KeyValueChangeRecord item in iteration.

6. KeyValueChangeRecord is the record representing the item change information. KeyValueChangeRecord has properties such as currentValue and previousValue. To get current value, we call its currentValue property and to get previous value, we call its previousValue property. Find the code snippet.
if (empArrayChanges) {
  empArrayChanges.forEachAddedItem((record) => {
	let emp = record.currentValue;
	console.log('Added ' + emp.name);
  });
} 

Detect Changes in Array

Suppose we have an array of Employee class and we want to detect changes in the array whenever an item is added and removed. We can detect changes using KeyValueDiffers as following.
@Input() empArray: Employee[];
constructor(private kvDiffers: KeyValueDiffers) {}
ngOnInit() {
  this.arrayDiffer = this.kvDiffers.find(this.empArray).create();
}
ngDoCheck() {
    let empArrayChanges = this.arrayDiffer.diff(this.empArray);
    if (empArrayChanges) {
      empArrayChanges.forEachAddedItem((record) => {
        let emp = record.currentValue;
        console.log('Added ' + emp.name);
      });
      empArrayChanges.forEachRemovedItem((record) => {
        let emp = record.previousValue;
        console.log('Removed ' + emp.name);
      });
    }
} 
The above code will not detect changes if any Employee object of the array changes value of its any property.

Detect Changes in Objects inside Array

Suppose we have an array of Employee class and we want to detect changes when any Employee object of the array changes value of its any property. We can detect changes using KeyValueDiffers as following.
@Input() empArray: Employee[];
empDifferMap = new Map<number, any>();
empMap = new Map<number, Employee>();
arrayDiffer: any;
constructor(private kvDiffers: KeyValueDiffers) {}
ngOnInit() {
    this.empArray.forEach(emp => {
      this.empDifferMap[emp.id] = this.kvDiffers.find(emp).create();
      this.empMap[emp.id] = emp;
    })
}
ngDoCheck() {
    for (let [key, empDiffer] of this.empDifferMap) {
      let empChanges = empDiffer.diff(this.empMap.get(key));
      if (empChanges) {
        empChanges.forEachChangedItem(record => {
          console.log('Previous value: ' + record.previousValue);
          console.log('Current value: ' + record.currentValue);
        });
      }
    }
} 

Complete Example

employee.ts
export class Employee {
	constructor(public id: number, public name: string){}
} 
employee.component.ts
import { Component, DoCheck, KeyValueDiffers, OnInit, Input } from '@angular/core';
import { Employee } from './employee';

@Component({
  selector: 'app-emp',
  template: `
    <h3>Change Logs</h3>
    <div *ngFor="let log of changeLogs">
      {{log}}
    </div> 
  `
})
export class EmployeeComponent implements DoCheck, OnInit {
  @Input()
  empArray: Employee[];

  empDifferMap = new Map<number, any>();
  empMap = new Map<number, Employee>();
  arrayDiffer: any;
  changeLogs: string[] = [];
  
  constructor(private kvDiffers: KeyValueDiffers) {
  }
  ngOnInit() {
    this.arrayDiffer = this.kvDiffers.find(this.empArray).create();

    this.empArray.forEach(emp => {
      this.empDifferMap[emp.id] = this.kvDiffers.find(emp).create();
      this.empMap[emp.id] = emp;
    })
  }
  ngDoCheck() {
    //Detect changes in array when item added or removed
    let empArrayChanges = this.arrayDiffer.diff(this.empArray);
    if (empArrayChanges) {
      console.log('... Array changes ...');
      this.changeLogs.push('... Array changes ...');      
      empArrayChanges.forEachAddedItem((record) => {
        let emp = record.currentValue;
        this.empDifferMap.set(emp.id, this.kvDiffers.find(emp).create());
        this.empMap.set(emp.id, emp);
        console.log('Added ' + emp.name);
        this.changeLogs.push('Added ' + emp.name);

      });
      empArrayChanges.forEachRemovedItem((record) => {
        let emp = record.previousValue;
        this.empDifferMap.delete(emp.id);
        this.empMap.delete(emp.id);
        console.log('Removed ' + emp.name);
        this.changeLogs.push('Removed ' + emp.name);
      });
    }

    //Detect changes in object inside array
    for (let [key, empDiffer] of this.empDifferMap) {
      let empChanges = empDiffer.diff(this.empMap.get(key));
      if (empChanges) {
        empChanges.forEachChangedItem(record => {
          console.log('--- Employee with id ' + key + ' updated ---');
          this.changeLogs.push('--- Employee with id ' + key + ' updated ---');
          console.log('Previous value: ' + record.previousValue);
          this.changeLogs.push('Previous value: ' + record.previousValue);
          console.log('Current value: ' + record.currentValue);
          this.changeLogs.push('Current value: ' + record.currentValue);
        });
      }
    }
  }
} 
app.component.ts
import { Component, OnInit } from '@angular/core';
import { Employee } from './employee';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="add()">Add</button> <br/>
    <button (click)="delete()">Delete</button> <br/>
    <button (click)="update()">Update</button>
    <app-emp [empArray]="empArray"></app-emp>
  `
})
export class AppComponent implements OnInit {
  empArray = [];
  index = 102;
  ngOnInit() {
    this.empArray.push(new Employee(100, "Mahesh"));
    this.empArray.push(new Employee(101, "Krishna"));
  }
  add() {
    this.empArray.push(new Employee(this.index++, "ABCD"));
    console.log('Employee added: ' + JSON.stringify(this.empArray));
  }
  delete() {
    if (this.empArray && this.empArray.length > 2) {
      this.empArray.pop();
      console.log('Employee deleted: ' + JSON.stringify(this.empArray));
    } else {
      console.log('No further delete.');
    }
  }
  nameCount = 1;
  update() {
    this.empArray[this.empArray.length - 1].name = "Shiva" + this.nameCount++;
  }
} 
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { EmployeeComponent } from './employee.component';

@NgModule({
      imports: [
            BrowserModule,
            FormsModule
      ],
      declarations: [
            AppComponent,
            EmployeeComponent
      ],
      providers: [
      ],
      bootstrap: [
            AppComponent
      ]
})
export class AppModule { } 

Output

Find the print-screen of the output.
Angular KeyValueDiffers + Detect Changes in Objects

References

Angular doc: KeyValueDiffers
Angular Lifecycle Hooks

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
LEARN MORE








©2024 concretepage.com | Privacy Policy | Contact Us