Angular NameOf in Component for String Safe and NgOnchanges

Viewed 495

What is the best way to compare NgOnChanges, while making it String Type safe in Angular? Currently this is what I am doing.

NgOnChanges SimpleChanges variable is datatype : any and accepts any variable name, even if misspelled. Trying to find clean way to prevent this.

Original:

else if (changes?.product?.previousValue != changes?.product?.currentValue) {
    this.submitSearch();

New:

export const nameof = <T>(name: keyof T) => name;

public readonly productMember = nameof<WebStoreSearchComponent>("product");

if ((changes[this.productMember]?.previousValue != changes[this.productMember]?.currentValue) {
    this.submitSearch();

Does Angular have any native way to conduct this?

Resources:

https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html

Note: Getters/setters are not a solution, since specifically asking about string safe for ngonchanges.

4 Answers

Last year I saw this video from ngConf, which suggested an approach of using decorator for something similar, which I thought was interesting. May be that will help you.

The idea is to create a decorator and use it for all required changes callback.

changes.decorator.ts

export function OnChanges<T>(callback: (newValue: T) => void) {
  const cachedValueKey = Symbol();
  return function(target, key) {
    Object.defineProperty(target, key, {
      set: function(value) {
        if (this[cachedValueKey] === value) {
          return;
        }

        this[cachedValueKey] = value;
        callback.call(this, value);
      },
      get: function() {
        return this[cachedValueKey];
      }
    });
  };
}

Usage in your case:

@OnChanges<string>((newValue: string) => {
  this.submitSearch();
})
@Input()
productMember: string;

take a look at this stackblitz.

There is no built-in support for this in Angular. A feature request was filed back in 2017 and is still open: https://github.com/angular/angular/issues/17560

You might want to adopt for your project some of the workarounds posted in that thread, as a custom solution.

Using getter/setter properties is a good alternative to ngOnChanges.

// tslint:disable-next-line: variable-name
private _productMember: string;

get productMember(): string {
  return this._productMember;
}
@Input()
set productMember(value: string) {
  const changed = value !== this._productMember;

  this._productMember = value;

  if (changed) {
    this.submitSearch();
  }
}

I had similar situation, I would liked to detect changes with a decorator, but on my own model. So I modified Kern Zhao's solution, maybe it will be useful for someone.

In JavaScript, objets are always stored by reference. That means one object is strictly equal another only if they both point to the same object in memory. Without this modification, basicly, comparson never will be true, that means you will got a bunch of fake change detection for sure.

For deeply equality checking, I used JSON.stringify(), which also have some limitations regarding properties ordering, but it works well on my computer.

onchanges.decorator.ts

export function OnChanges<T>(callback: (newValue: T) => void) {
  const cachedValueKey = Symbol();
  return function (target, key) {
    Object.defineProperty(target, key, {
      set: function (value) {
        if (JSON.stringify(this[ cachedValueKey ]) === JSON.stringify(value)) {
          return;
        }

        this[ cachedValueKey ] = value;
        callback.call(this, value);
      },
      get: function () {
        return this[ cachedValueKey ];
      }
    });
  };
}

Usage:

@OnChanges<Person>(function(newValue: Person) {
  this.submitSearch();
  this.doSomething('Do something with it', newValue);
})
@Input()
productMember: Person;
Related