Sposób na śledzenie zmian property @Input - ngOnChanges i Angular
Metoda ngOnChanges
bardzo często przydaje się do nadsłuchiwania zmian w polach @Input
. Jeżeli chcemy się dowiedzieć kiedy zmienia się wartość takiego property, lifecycle hook ngOncChanges
może się do tego przydać.
Ogólne działanie ngOnChanges
Zacznijmy jednak od początku. Metoda ngOnChanges
pochodzi z interfejsu OnChanges
. Jest to metoda, która w cyklu życia komponentu wywoływana jest jako pierwsza, zaraz po konstruktorze. Wywoływana jest także za każdym razem, gdy property @Input
zmieni się przez przekazanie nowej wartości z komponentu położonego wyżej.
Metoda ngOnChages
wygląda tak:
ngOnChanges(changes: SimpleChanges): void {}
Jako parametr przyjmuje obiekt SimpleChanges
, który jest tablicą asocjacyjną wszystkich property znajdujących się w naszym komponencie. Za pomocą klucza typu string możemy z obiektu changes
wyciągnąć obiekt SimpleChange
, który reprezentuje dokładnie property @Input
w naszym komponencie. Jeżeli w komponencie mamy property @Input name
, to możemy je z obiektu SimpleChanges wyciągnąć w taki sposób:
let prop: SimpleChange = changes['name']
Obiekt reprezentujący zmianę property @Input
wygląda tak:
class SimpleChange { constructor(previousValue: any, currentValue: any, firstChange: boolean) previousValue: any currentValue: any firstChange: boolean isFirstChange(): boolean }
Mamy więc w tym miejscu dostęp do poprzedniej i obecnej wartości, a także dodatkowe informacje o pierwszej zmianie obiektu.
Możemy też użyć prostej pętli i sprawdzić wszystkie obiekty:
ngOnChanges(changes: SimpleChanges) { for (const prop of Object.keys(changes)) { console.log(changes[prop]); } }
Rozważmy prosty komponent, który posiada jedno property @Input name
.
export class ChildComponent implements OnChanges { @Input() name ngOnChanges(changes: SimpleChanges) { for (const prop of Object.keys(changes)) { console.log(changes[prop]) } } }
Jeżeli zainicjalizujemy property z komponentu położonego wyżej:
<app-child name="Angular"></app-child>
W konsoli otrzymujemy informację o tym, że nastąpiła zmiana wartości:
previousValue: undefined currentValue: "Angular" firstChange: true
Właśnie w taki sposób, możemy śledzić zmianę wartości pól @Input
w klasie komponentu. Jeżeli w naszym komponencie będziemy mieli inne property @Input
ale nie będzie zainicjalizowane, to SimpleChanges
nie będzie posiadał o nim żadnej informacji. Ponieważ SimpleChanges
posiada informacje tylko o tych property, które się zmieniają i o zmianach, które potrafi wykryć.
Jeżeli chcemy śledzić tylko konkretne property to musimy się posłużyć na przykład takim zapisem:
ngOnChanges(changes: SimpleChanges) { if (changes.name) { console.log('Nowa wartość "@name": ', changes.name.currentValue); } }
To jest jeden ze sposobów śledzenia zmian w property @Input
w Angularze, ale nie jedyny.
Śledzenie zmian @Input
za pomocą getters/setters
Innym sposobem śledzenia zmian w polach@Input
jest użycie czystego TypeScript oraz set
i get
.
private _company; @Input() set company(value: string) { this._company = value; console.log(`title is changed to ${value}`); } get company(): string { return this._company; }
Przy takim sposobie, musimy stworzyć prywatne pole w klasie komponentu i odpowiednio akcesor set
i get
dla tego pola klasy. Za każdym razem gdy komponent wyższego rzędu, będzie chciał użyć tego pola musi użyć nazwy akcesora:
<app-child company="Google"></app-child>
Będziemy mogli odnotować zmianę przy każdej zmianie property. Oczywiście ma to swoje plusy i minus. Musimy dopisać większą ilość kodu i posługiwać się dodatkowym polem prywatnym, które i tak nie jest prywatne bo stworzyliśmy dla niego set
i get
:-D.
Jednak ten sposób ma jedną ważną zaletę, możemy wyśledzić zmianę nawet jeżeli metoda ngOnChanges
nie odnotuje zmiany. Może się tak wydarzyć, gdy zmienimy pole @Input
wewnątrz klasy komponentu lub też będziemy pracować na obiekcie.
Kiedy ngOnChanges
nie odnotuje zmiany
Może się tak zdarzyć, że metoda ngOnChanges
nie odnotuje zmiany w polu @Input
. Dzieje się tak gdy postanowimy nadpisać property np. w metodzie ngOnInit
, rozważmy taki przykład:
ngOnInit() { this.name = 'React'; }
W tym przypadku podmieniam wartość @Input name
na inną w ngOnInit
. Pole @Input name
cały czas jest inicjalizowane z komponentu położonego wyżej i ngOnChanges
widzi inicjalizacyjną zmianę property, ale nie odnotowuje nadpisania w metodzie ngOnInit
. Oczywiście w widoku HTML widnieje napis React i strona renderuje prawidłową wartość, jednak ngOnChanges
jest w tym przypadku głuchy.
SimpleChange {previousValue: undefined, currentValue: "Angular", firstChange: true}
Property @Input color
zmieniła wartość, ale nie zostało to odnotowane przez metodę ngOnChanges
, tylko wartość inicjalizacyjna została odnotowana. Metoda ta uruchamia się tylko wtedy, gdy zmiana pól @Input
przychodzi przez bindowanie w widoku HTML.
Dlatego użycie akcesora set
może się przydać jeżeli chcemy sprawdzać zmieniające się wartości gdy modyfikujemy property @Input
inaczej niż przez bindowanie w widoku HTML.
NgOnChanges i referencje
Inną sytuacją kiedy ngOnChanges
nie powiadomi nas o zmianach są obiekty referencyjne. Gdy zmienia się tylko zawartość obiektu lub listy, Angular nie uruchomi metody ngOnChanges
. Dodaję do przykładu nowy @Input
:
@Input() frameworks: Array<string>;
Nadrzędny komponent wygląda tak:
@Component({ selector: 'app-root', template: ` <app-child name="Angular" [frameworks]="others"></app-child> <button (click)="add()">Dodaj</button> `, }) export class AppComponent { others = ['Vue', 'React'] add() { this.others.push('Svelte') // Należy odkomentować, aby Angular wykrył zmianę // i odnotowałę w ngOnChanges // this.others = [...this.others]; } }
Lista others
zbindowana jest do @Input() frameworks
. Na początku wszystko inicjalizuje się poprawnie i ngOnChanges
odnotowuje zmianę:
SimpleChange {previousValue: undefined, currentValue: Array(2), firstChange: true} previousValue: undefined currentValue: (2) ["Vue", "React"] firstChange: true
Gdy za pomocą przycisku dodamy kolejną wartość do listy, ngOnChanges
nie wykrywa takiej zmiany. Zmiany oczywiście renderowane są w widoku HTML ponieważ Change Detection w Angularze działa, ale ngOnChanges
milczy.
W przypadku obiektów ngOnChanges
potrafi wykryć tylko zmieniającą się referencję. Nie jest w stanie sprawdzić czy coś mogło się zmienić w samym obiekcie.
Sposobem na to jest przekazanie nowej referencji. W ten sposób metoda ngOnChanges
odnotuje zmianę w property @Input
:
add() { this.others.push('Svelte'); this.others = [...this.others]; }
Podsumowanie ngOnChanges
ngOnChanges
uruchamia się zaraz po konstruktorze i przedngOnInit
ngOnChanges
może się przydać do śledzenia zmian w property@Input
- alternatywnym rozwiązaniem śledzenia zmian jest użycie
set
/get
dla property@Iput
ngOnChanges
uruchamia się za każdym razem, gdy property@Input
zmieni wartość w widoku HTML gdzie jest bindowanangOnChanges
nie uruchamia się gdy zmieniają się pola obiektów lub zawartość listy (nie obsługuje zmian w obiektach referencyjnych)- aby
ngOnChanges
odnotował zmiany przy obiektach, musimy przekazać je jako nową referencję ngOnChanges
nie uruchomi się gdy zmienimy wartość property@Input
przez bezpośrednie odniesienie np.this.color = 'red'
Przydatne linki
Mój przykład na StackBlitz: https://stackblitz.com/edit/przyklad-ng-on-changes
https://angular.io/guide/lifecycle-hooks#onchanges
https://angular.io/api/core/SimpleChange
https://medium.com/angular-in-depth/creatively-decouple-ngonchanges-fab95395cc6e