Angular By Solutions - Part-1

Angular is a large framework offering a variety of tools to create a solution. There are many ways to achieve the same thing. One can easily get confused about the path they should choose for a particular situation.

This series is dedicated to solve some of the conundrum an Angular developer faces. Each part comprises of a problem situation and a few proposed solutions . Examples and code references will be according to Angular Version 7. Hope you like it.

  • Common imports & decorators are omitted from code samples for simplicity.

Situation 1: How to monitor changes of component properties?

Angular project is basically a tree of components & the common functionality of a component is to declare some properties, bind them to view , watch for changes and do some task when the value is changed.

Angular by default updates the property value attached with the model (Not for cases like ngModelChange when we have to manually update the property). We need to detect the change to hook up some functionality.

Now there are multiple ways to detect changes for models, below are some possible solutions.

Method 1: Using a marker for the model we want to detect changes.

Check the example below.

foo.component.html
<div class="box">
  <form name="form1">
    <input type="text" name="fname" [(ngModel)]="fname" #fNameModel="ngModel">
  </form>
</div>
foo.component.ts
import { Component, AfterViewInit, ViewChild } from '@angular/core';
import { NgModel } from '@angular/forms';

class FooComponent implements AfterViewInit {
  fname: string;
  // get a reference of marker we declared in the view
  @ViewChild('fNameModel', {static: false}) _fNameModel: NgModel; 

  ngAfterViewInit() {
    // watch for changes in the model
    this._fNameModel.valueChanges.subscribe((value) => {
      // value is changed
      console.log('value->', value);
    });
  }
}

Note: Subscription must be defined within ngAfterViewInit block. Otherwise you will get a Cannot read property 'valueChanges' of undefined error.

Method 2: Using getter and setter.

foo.component.html
<div class="box">
  <form name="form1">
    <input type="text" name="fname" [(ngModel)]="fname">
  </form>
</div>
foo.component.ts
import { Component, Input } from '@angular/core';

class FooComponent {
  private _fname; // shadow property
  get fname() {
    return this._fname;
  }
  @Input // use only if property is a component Input
  set fname(value: string){
    this._fname = value;
    // value is changed
    console.log('value->', value);
  }
}
What's bad?
  • Increased line of code per property.
  • Use of an extra shadow property.
  • The shadow property can be modified from anywhere in the component, not just the setter function.

Method 3: Using one way style binding for the model in view and using ngModelChange event.

foo.component.html
<div class="box">
  <form name="form1">
    <input type="text" name="fname" [ngModel]="fname" (ngModelChange)="fnameChanged($event);">
  </form>
</div>
foo.component.ts
class FooComponent {
  fname: string;
  fnameChanged(value) {
   this.fname = value; // we must manually update the model
   // value is changed
   console.log('value->', value);
  }
}

Method 4: Using OnChanges lifecycle event. Angular fires this event when properties decorated with @Input() changes.

app.component.html
<div class="app">
  <card [cardName]="cardNameFromApp"></card>
</div>
app.component.ts
import { Component, OnInit } from '@angular/core';

export class AppComponent implements OnInit {

  cardNameFromApp = 'Card 1';
  
  ngOnInit() {
   // change the cardName to see it updated in the card component
   setTimeout(() => {
     this.cardNameFromApp = 'Card 2';
   }, 4000);
  }
}
card.component.html
<div class="card">
  {{ cardName }}
</div>
card.component.ts
import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';

@Component({
  selector: 'card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent implements OnChanges {

  @Input() cardName: string;

  ngOnChanges(changes: SimpleChanges) {
    if (changes.cardName) {
       // value is changed
    console.log('changes', changes.cardName.previousValue,  changes.cardName.currentValue);
    }
  }

}
What's bad?
  • It combines change detection of ALL input properties into one ngOnChanges hook function. And then we need to separate those properties with an if statement making it less readable especially when there are many properties to be watched.
  • The interface of SimpleChanges accepts any string as its key, making it possible for typos. For example, changes.typo_key will not be complained about by the TypeScript compiler.
  • SimpleChange.previousValue and SimpleChange.currentValue are typed to any instead of the desired property type.

The above method works fine with primitive data types such as string or number. For Arrays & Objects ngOnChanges will fail to detecet any change (if we change value of keys) untill we change the whole reference. See below example to understand the problem.

app.component.html
<div class="app">
  <card [config]="cardConfig"></card>
</div>
app.component.ts
import { Component, OnInit } from '@angular/core';

export class AppComponent implements OnInit  {

  cardConfig = {
    name: 'Card 1'
  }

  ngOnInit() {
    setTimeout(() => {
      // update object property
      this.cardConfig.name = 'Card 2';
    }, 4000)
  }
}
card.component.ts
import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';

export class CardComponent implements OnChanges {

  @Input() config: any;

  ngOnChanges(changes: SimpleChanges) {
    if (changes.config) {
      // angular will update the model value
      // but ngOnChanges will not detect it
      console.log('changes', changes.config.previousValue, changes.config.currentValue);
    }
  }

}

In the above example we have taken an @Input called config in Card Component. It's an object. So when the parent component modifies value for any of it's keys, Card Component will not be able to detect any changes.

When we modify value of an object key, reference in the memory for that object does not changes, therefor Angular can't detect any changes. Same will happen for Array type properties. To make it possible we have to put custom change detection logic via Angular's DoCheck lifecycle method.

Otherwise we can make it work by changing the whole reference like below.

app.component.ts
 ngOnInit() {
    setTimeout(() => {
      // changing the reference by assignning a new object
      const temp = { name: 'Card 2' };
      this.cardConfig = temp;
    }, 4000)
  }

Method 6: Using DoCheck lifecycle event.

DoCheck allows us to write custom change detection logic in our components. Components implenting DoCheck interface must define a method called ngDoCheck.

  • WARNING!
  • ngDoCheck fires on every change detection cycle, do not write
  • any complex logic otherwise performance of your application may hamper.

Example

app.component.html
<div class="app">
  <card [config]="cardConfig"></card>
</div>
app.component.ts
import { Component, OnInit } from '@angular/core';

export class AppComponent implements OnInit  {

  cardConfig = {
    name: 'Card 1'
  }

  ngOnInit() {
    setTimeout(() => {
      this.cardConfig.name = 'Card 2';
    }, 4000)
  }
}
card.component.ts
 import { Component, KeyValueDiffer, KeyValueDiffers, OnInit, DoCheck } from '@angular/core';

@Component({
  selector: 'card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit, DoCheck {

  @Input() config: any;
  configDiffer: KeyValueDiffer<string, any>;

  constructor(
    private _differs: KeyValueDiffers
  ) {}

  ngOnInit() {
    // create a differ object for ngDoCheck
    this.configDiffer = this._differs.find(this.config).create();
  }

  ngDoCheck(): void {
    // diff current config model with one stored in diff object
    if (this.configDiffer) {
      const changedValues = this.configDiffer.diff(this.config);
      if (changedValues) {
        // value is changed
        changedValues.forEachChangedItem((item) => {
          console.log('changedItem', item.key, item.previousValue, item.currentValue);
        });
        // other methods available in objChanges
        changedValues.forEachAddedItem((item) => ...);
        changedValues.forEachRemovedItem((item) => ...);
      }
    }
  }

}
What's good?
  • Changes on both primitive as well as object and array type properties can be detected.
  • Changes of all properties including @Input can be watched from one place.
What's bad?
  • Complex implementation
  • Will execute on every change detection cycle.

Method 7: Using typescript experimental decorators. Decorators implies the theory of meta programming. Check the example below.

WARNING: Typescript's decorator API is experimental. It's defination is continuasly changing with each new version. So use with caution.

onchange.custom.decorator.ts
// Our own `SimpleChange` interface with a generic type T
export interface SimpleChange<T> {
  firstChange: boolean;
  previousValue: T;
  currentValue: T;
  isFirstChange: () => boolean;
}

export function DetectChanges<T = any>(callback: (value: T, simpleChange?: SimpleChange<T>) => void) {
  const cachedValueKey = Symbol();
  const isFirstChangeKey = Symbol();
  return (target: any, key: PropertyKey) => {
    Object.defineProperty(target, key, {
      set: function (value) {
        // change status of "isFirstChange"
        this[isFirstChangeKey] = (this[isFirstChangeKey] === undefined) ? true : false;
        // No operation if new value is same as old value
        if (!this[isFirstChangeKey] && this[cachedValueKey] === value) {
          return;
        }
        const oldValue = this[cachedValueKey];
        this[cachedValueKey] = value;
        // create our own SimpleChange object
        const simpleChange: SimpleChange<T> = {
          firstChange: this[isFirstChangeKey],
          previousValue: oldValue,
          currentValue: this[cachedValueKey],
          isFirstChange: () => this[isFirstChangeKey],
        };
        callback.call(this, this[cachedValueKey], simpleChange);
      },
      get: function () {
        return this[cachedValueKey];
      },
    });
  };
}
card.component.ts
import { DetectChanges } from '../custom-decorators/onchange.custom.decorator';

export class CardComponent {
  @DetectChanges<string>(function (value, simpleChange) {
      console.log(`cardName is changed to: ${value}`);
  })
  @Input()
  cardName: string;
}
What's good?
  • Easy to use, less code, better readability.
  • Hide _cachedValue from developer, no more “shadow property”.
  • Better typing. SimpleChange.previousValue is typed to a generic type.
  • It can also be used with a non-@Input property.
  • It’s not specific to Angular. So it can be used as long as it’s TypeScript such as React in TypeScript.

Thanks to Siyang for references on Typescript decorators.

What next?

You can contact us for your software and consultancy requirements.

© 2024, Attosol Private Ltd. All Rights Reserved.