How to detect when an @Input() value changes in Angular?
Asked Answered
L

21

864

I have a parent component (CategoryComponent), a child component (videoListComponent) and an ApiService.

I have most of this working fine i.e. each component can access the json api and get its relevant data via observables.

Currently video list component just gets all videos, I would like to filter this to just videos in a particular category, I achieved this by passing the categoryId to the child via @Input().

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

This works and when the parent CategoryComponent category changes then the categoryId value gets passed through via @Input() but I then need to detect this in VideoListComponent and re-request the videos array via APIService (with the new categoryId).

In AngularJS I would have done a $watch on the variable. What is the best way to handle this?

Lorri answered 25/7, 2016 at 15:28 Comment(2)
angular.io/docs/ts/latest/cookbook/…Nguyen
for array changes: #42962894Shiv
O
1350

Actually, there are two ways of detecting and acting upon when an input changes in the child component in angular2+ :

  1. You can use the ngOnChanges() lifecycle method as also mentioned in older answers:
    @Input() categoryId: string;
        
    ngOnChanges(changes: SimpleChanges) {
        
        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values
        
    }
    

Documentation Links: ngOnChanges, SimpleChanges, SimpleChange
Demo Example: Look at this plunker

  1. Alternately, you can also use an input property setter as follows:
    private _categoryId: string;
    
    @Input() set categoryId(value: string) {
    
       this._categoryId = value;
       this.doSomething(this._categoryId);
    
    }
    
    get categoryId(): string {
    
        return this._categoryId;
    
    }

Documentation Link: Look here.

Demo Example: Look at this plunker.

WHICH APPROACH SHOULD YOU USE?

If your component has several inputs, then, if you use ngOnChanges(), you will get all changes for all the inputs at once within ngOnChanges(). Using this approach, you can also compare current and previous values of the input that has changed and take actions accordingly.

However, if you want to do something when only a particular single input changes (and you don't care about the other inputs), then it might be simpler to use an input property setter. However, this approach does not provide a built in way to compare previous and current values of the changed input (which you can do easily with the ngOnChanges lifecycle method).

EDIT 2017-07-25: ANGULAR CHANGE DETECTION MAY STILL NOT FIRE UNDER SOME CIRCUMSTANCES

Normally, change detection for both setter and ngOnChanges will fire whenever the parent component changes the data it passes to the child, provided that the data is a JS primitive datatype(string, number, boolean). However, in the following scenarios, it will not fire and you have to take extra actions in order to make it work.

  1. If you are using a nested object or array (instead of a JS primitive data type) to pass data from Parent to Child, change detection (using either setter or ngchanges) might not fire, as also mentioned in the answer by user: muetzerich. For solutions look here.

  2. If you are mutating data outside of the angular context (i.e., externally), then angular will not know of the changes. You may have to use ChangeDetectorRef or NgZone in your component for making angular aware of external changes and thereby triggering change detection. Refer to this.

Oni answered 21/6, 2017 at 20:53 Comment(13)
Very good point regarding using setters with inputs, I will update this to be the accepted answer as it is more comprehensive. Thanks.Lorri
@trichetriche, the setter (set method) will get called whenever the parent changes the input. Also, the same is true for ngOnChanges() as well.Oni
Well apparently not ... I'll try to figure this out, it's probably something wrong I did.Waldenses
@trichetriche, Please look at EDIT 2017-07-25 in my answer above as to why change detection may not be firing in your case. Most likely, it might be reason No 1 listed in the edit.Oni
If we depend ngOnInit execute something operator, the setter is not wrok.Frigg
I don't know, it was frigging 3 months ago ! Why would you unbury topics like that ?Waldenses
I just stumbled upon this question, noted that you said using the setter won't allow to compare values, however that's not true, you can call your doSomething method and take 2 arguments the new and old values before actually setting the new value, another way would be storing the old value before setting and calling the method after that.Weide
@Weide What I am saying is that input setter does not natively support comparing old/new values like ngOnChanges() does. You can always use hacks to store the old value (as you mention) to compare it with the new value.Oni
Hello.I want to ask something about the 2nd approach (setter).Let's say I have an object a which has a nested object b a = {b:SomeType}.On the first run b is null but after some seconds it can take a value.How can I observe when b receives a value?Tsushima
I am sorry for commenting on the off topic issue but can you please help me out for my this question for which i am looking it's answer since three days: #63722949Germin
If not nested properties change in an object, but the object pointer itself is changed, will using the setter work?Stubble
Here is an example of the difference in how the "setter" behaves when passing in the whole object vs. mutating an existing object by just updating one of its properties: stackblitz.com/edit/angular-q5ixah?file=src/app/…Repro
Bravo. This should be somewhere in the oficial documentation or tutorials.Salonika
A
128

Use the ngOnChanges() lifecycle method in your component.

ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed.

Here are the Docs.

Alissaalistair answered 25/7, 2016 at 15:31 Comment(2)
This works when a variable is set to a new value e.g. MyComponent.myArray = [], but if an input value is altered e.g. MyComponent.myArray.push(newValue), ngOnChanges() is not triggered. Any ideas about how to capture this change?Minute
ngOnChanges() isn't called when a nested object has changed. Maybe you find a solution hereAlissaalistair
Y
72

I was getting errors in the console as well as the compiler and IDE when using the SimpleChanges type in the function signature. To prevent the errors, use the any keyword in the signature instead.

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

EDIT:

As Jon pointed out below, you can use the SimpleChanges signature when using bracket notation rather than dot notation.

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
Yellowstone answered 6/10, 2016 at 19:47 Comment(6)
Did you import the SimpleChanges interface at the top of your file?Lorri
@Jon - Yes. The problem wasn't the signature itself, but accessing the parameters that are assigned at runtime to the SimpleChanges object. For example, in my component I bound my User class to the input (i.e. <my-user-details [user]="selectedUser"></my-user-details>). This property is accessed from the SimpleChanges class by calling changes.user.currentValue. Notice user is bound at runtime and not part of the SimpleChanges objectYellowstone
Have you tried this though? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }Lorri
@Jon - Switching to bracket notation does the trick. I updated my answer to include that as an alternative.Yellowstone
import { SimpleChanges } from '@angular/core'; If it's in the angular docs, and not a native typescript type, then it's probably from an angular module, most likely 'angular/core'Fullgrown
Does currentValue show the value before the change or after?Harpy
S
28
@Input() set categoryId(categoryId: number) {
      console.log(categoryId)
}

please try using this method. Hope this helps

Shorter answered 12/12, 2018 at 12:47 Comment(1)
This definitely helps!Mauser
A
22

The safest bet is to go with a shared service instead of a @Input parameter. Also, @Input parameter does not detect changes in complex nested object type.

A simple example service is as follows:

Service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }
  
  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

You can use a similar implementation in other components and all your compoments will share the same shared values.

Achondrite answered 30/11, 2017 at 11:24 Comment(5)
Thankyou Sanket. This is a very good point to make and where appropriate a better way of doing it. I will leave the accepted answer as it stands as it answers my specific questions regarding detecting @Input changes.Lorri
An @Input parameter does not detect changes inside a complex nested object type, but if the object itself changes, it will fire, right? e.g. I have the input parameter "parent", so if I change parent as a whole, change detection will fire, but if I change parent.name, it won't?Flashover
You should provide a reason why your solution is saferHeindrick
@JoshuaKemmerer @Input parameter does not detect changes in complex nested object type but it does in simple native objects.Achondrite
Here is an example of the difference in how the "setter" behaves when passing in the whole object vs. mutating an existing object by just updating one of its properties: stackblitz.com/edit/angular-q5ixah?file=src/app/…Repro
W
18

Angular ngOnChanges

The ngOnChanges() is an inbuilt Angular callback method that is invoked immediately after the default change detector has checked data-bound properties if at least one has changed. Before the view and content, children are checked.

// child.component.ts

import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges {

  @Input() inputParentData: any;

  constructor() { }

  ngOnInit(): void {
  }

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

}

for more: Angular Docs

Washbowl answered 10/8, 2021 at 17:40 Comment(0)
S
9
export class ChildComponent implements OnChanges {
  @Input() categoryId: string;

  ngOnChanges(changes: SimpleChanges) {
    if (changes.categoryId) { // also add this check 
      console.log('Input data changed:', this.categoryId);
    }
  }
}

When the input is changed, changedetection calls ngOnChanges. changes: SimpleChanges object has all the changes done. Check if categoryId is one of the changes happened. If yes, do what you need to do.

Sapele answered 3/4, 2023 at 8:22 Comment(0)
F
8

I just want to add that there is another Lifecycle hook called DoCheck that is useful if the @Input value is not a primitive value.

I have an Array as an Input so this does not fire the OnChanges event when the content changes (because the checking that Angular does is 'simple' and not deep so the Array is still an Array, even though the content on the Array has changed).

I then implement some custom checking code to decide if I want to update my view with the changed Array.

Footstone answered 14/8, 2017 at 11:9 Comment(0)
S
8

I'd stick to approach, suggested by @alan-c-s, but with some modifications. First - I'm against using ngOnChanges. Instead, I propose to move all what needs to be changed under one object. And use BehaviorSubject to track it changes:

  private location$: BehaviorSubject<AbxMapLayers.Location> = new BehaviorSubject<AbxMapLayers.Location>(null);

  @Input()
  set location(value: AbxMapLayers.Location) {
    this.location$.next(value);
  }
  get location(): AbxMapLayers.Location {
    return this.location$.value;
  }

<abx-map-layer
    *ngIf="isInteger(unitForm.get('addressId').value)"
    [location]="{
      gpsLatitude: unitForm.get('address.gpsLatitude').value,
      gpsLongitude: unitForm.get('address.gpsLongitude').value,
      country: unitForm.get('address.country').value,
      placeName: unitForm.get('address.placeName').value,
      postalZip: unitForm.get('address.postalZip').value,
      streetName: unitForm.get('address.streetName').value,
      houseNumber: unitForm.get('address.houseNumber').value
    }"
    [inactive]="unitAddressForm.disabled"
    >
</abx-map-layer>
Sri answered 30/3, 2022 at 16:13 Comment(0)
S
7

None of the answers seem to be the best solution. They are overly-complicated even if they do ultimately work.

All that you need to do is use a get-set pattern, also known as "auto-properties", on the input variable like the following:

    ...
    private _categoryId: number;

    @Input()
    set categoryId(value: number) {
      this._categoryId = value;
      // Do or call whatever you want when this input value changes here
    }
    get categoryId(): number {
      return this._categoryId;
    }
    ...

Then anywhere else in the code, this can be referenced as this.categoryId because the getter automatically gets called when referencing that in TypeScript.

Further reading:

Saudra answered 29/3, 2023 at 17:32 Comment(0)
U
5

You can also , have an observable which triggers on changes in the parent component(CategoryComponent) and do what you want to do in the subscribtion in the child component. (videoListComponent)

service.ts

public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

CategoryComponent.ts

public onCategoryChange(): void {
  service.categoryChange$.next();
}

videoListComponent.ts

public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
  });
}
Unmeaning answered 19/9, 2017 at 22:39 Comment(0)
P
5

Here ngOnChanges will trigger always when your input property changes:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}
Phallic answered 15/4, 2019 at 11:6 Comment(0)
D
5

This solution uses a proxy class and offers the following advantages:

  • Allows the consumer to leverage the power of RXJS
  • More compact than other solutions proposed so far
  • More typesafe than using ngOnChanges()
  • You can observe any class field this way.

Example usage:

@Input()
num: number;
@Input()
str: number;
fields = observeFields(this); // <- call our utility function

constructor() {
  this.fields.str.subscribe(s => console.log(s));
}

Utility functions:

import { BehaviorSubject, Observable, shareReplay } from 'rxjs';

const observeField = <T, K extends keyof T>(target: T, key: K) => {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get: () => subject.getValue() as T[K],
    set: (newValue: T[K]) => {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
};

export const observeFields = <T extends object>(target: T) => {
  const subjects = {} as { [key: string]: Observable<any> };
  return new Proxy(target, {
    get: (t, prop: string) => {
      if (subjects[prop]) { return subjects[prop]; }
      return subjects[prop] = observeField(t, prop as keyof T).pipe(
        shareReplay({refCount: true, buffer:1}),
      );
    }
  }) as Required<{ [key in keyof T]: Observable<NonNullable<T[key]>> }>;
};

Demo

Dachshund answered 20/4, 2020 at 18:30 Comment(7)
Note that if the values emitted are objects, subject will always emit.Parsimony
@LuisDiegoHernández, I'm not entirely sure what you mean. It uses a shallow equality comparison to determine whether a new value should be emitted.Dachshund
Nice, and here is a version where you can observe twice the same property without overriding the previous observer.Quintal
@FlavienVolken nice :) I have actually since been using an updated version in my code. I've now revised the above answer accordingly. This addresses the issue which you (rightfully) pointed out. It is also a bit easier to use because you no longer need to define a class-level observable field for every field you want to observe.Dachshund
I had a similar approach for a whole class by extending a proxy (yes, normally we cannot do this). Also, I would use shareReplay({refCount: true, buffer:1}) in your code to make sure there is no memory leak in the subscriptions. Last thing is that I prefer keeping the current input value instead of overriding it with the BehaviorSubject, but both approach are valid.Quintal
Well… actually a Subject + a .shareReplay() can be directly achieved using a ReplaySubject :-)Quintal
@FlavienVolken you genuinely educated me :) good point, thanks. I need to take some time to just ensure that switching to a replaysubject works as it did. For now, will update my shareReplay code as advised.Dachshund
C
3

You could also just pass an EventEmitter as Input. Not quite sure if this is best practice tho...

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

And in VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}
Closefisted answered 20/9, 2019 at 9:1 Comment(1)
I added an example.Anoxia
B
2

You can use a BehaviorSubject within a facade service then subscribe to that subject in any component and when an event happens to trigger a change in data call .next() on it. Make sure to close out those subscriptions within the on destroy lifecycle hook.

data-api.facade.ts

@Injectable({
  providedIn: 'root'
})
export class DataApiFacade {

currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);

}

some.component.ts

constructor(private dataApiFacade: DataApiFacade){}

ngOnInit(): void {
  this.dataApiFacade.currentTabIndex
    .pipe(takeUntil(this.destroy$))
       .subscribe(value => {
          if (value) {
             this.currentTabIndex = value;
          }
    });
}

setTabView(event: MatTabChangeEvent) {
  this.dataApiFacade.currentTabIndex.next(event.index);
}

ngOnDestroy() {
  this.destroy$.next(true);
  this.destroy$.complete();
}
Bainter answered 7/8, 2020 at 15:39 Comment(0)
M
2

Basically both suggested solutions work fine in most cases. My main negative experience with ngOnChange() is the lack of type safety.

In one of my projects I did some renaming, following which some of the magic strings remained unchanged, and the bug of course took some time to surface.

Setters do not have that issue: your IDE or compiler will let you know of any mismatch.

Maronite answered 29/1, 2021 at 22:34 Comment(0)
B
2

You can use the ngOnChanges() lifecycle method

@Input() inputValue: string;

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['inputValue'].currentValue);
}
Barri answered 21/3, 2021 at 18:10 Comment(0)
A
2
 @Input() public set categoryId(id: number) {
    if (id) {
    console.log(id)
    }
 }
Aframe answered 20/7, 2023 at 16:55 Comment(1)
Thank you for your interest in contributing to the Stack Overflow community. This question already has quite a few answers—including one that has been extensively validated by the community. Are you certain your approach hasn’t been given previously? If so, it would be useful to explain how your approach is different, under what circumstances your approach might be preferred, and/or why you think the previous answers aren’t sufficient. Can you kindly edit your answer to offer an explanation?Deliladelilah
B
1

If you're dealing with the case that your are using @Input to share data between parent and child component, you can detect @Input data changes by using the lifecycle method: ngOnChanges

 ngOnChanges(changes: SimpleChanges) {
    if (!changes.categoryId.firstChange) {
      // only logged upon a change after rendering
      console.log(changes.categoryId.currentValue);
    }
  }

And I'm advising that you should care of the change strategy implemented for the child component you should add ChangeDetectionStrategy.OnPush for some performance reason :

Example-Code :

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoListComponent implements OnChanges {
  @Input() categoryId: string;
Became answered 13/7, 2021 at 22:12 Comment(0)
I
0

So I wanted to create a custom directive for the so I can standardize my error messages like this:

<mat-error FormError [control]="formgroup.get('mycontrol') name="Streetaddress"></mat-error>

I ended up reading a lot of community input and ended up using the ngDoCheck function. The code below is a very crude example but it works. It could be improved upon by e.g. checking if there actual was a change.

import {Directive, DoCheck, ElementRef, HostListener, Input} from '@angular/core';
import {AbstractControl} from "@angular/forms";
import {FormErrorSupportService} from "./form-error-support.service";

@Directive({
  selector: '[FormError]'
})
export class FormErrorDirective  implements  DoCheck{
  @Input() control:AbstractControl;
  @Input() name: string;

  @HostListener('change') logChange() {this.detectChanges(); }

  constructor(
    private el: ElementRef
  ) {

  }

  detectChanges(): void {
    if (this.control.invalid) {
      this.el.nativeElement.style.display = 'block';
      this.el.nativeElement.innerHTML = FormErrorSupportService.report(this.control, this.name);
    }
    else {
      this.el.nativeElement.innerHTML = ''
    }

  }

  ngDoCheck(): void {
    console.log(`change detection`);
    this.detectChanges();
  }


}

The FormErrorSupportService has a static method that will actually construct the error text based on the error property. Messages are in dutch, but you'll get the gist.

import { Injectable } from '@angular/core';
import {AbstractControl} from "@angular/forms";

@Injectable({
  providedIn: 'root'
})
export class FormErrorSupportService {

  constructor() { }

  public static report(control: AbstractControl, name: string): string {
    console.log(`${name}: `, control?.errors);

    if (control === null ) { return '';}
    if (control.valid) { return '';}

    if (control.hasError('required')) {
      return $localize`Het veld ${name} mag niet leeg zijn.`;
    }
    else if (control.hasError('min')) {
      return $localize`Het veld ${name} moet een minimale waarde van ${control.errors['min'].min} hebben.`;
    }
    else if (control.hasError('max')) {
      return $localize`Het veld ${name} moet een maximale waarde van ${control.errors['max'].max} hebben.`;
    }
    else if (control.hasError('pattern')) {
      return $localize`Het veld ${name} voldoet niet aan het gewenste patroon.`;
    }
    else if (control.hasError('maxlength')) {
      return $localize`Het veld ${name} mag maximaal ${control.errors['maxlength'].requiredLength} tekens lang zijn.`;
    }
    else if (control.hasError('minlength')) {
      return $localize`Het veld ${name} moet minimale lengte van ${control.errors['minlength'].requiredLength} tekens zijn.`;
    }
    else {
      return 'error?';
    }

  }
}
Idell answered 15/12, 2023 at 8:59 Comment(0)
B
-1

If you don't want use ngOnChange implement og onChange() method, you can also subscribe to changes of a specific item by valueChanges event, ETC.

myForm = new FormGroup({
  first: new FormControl(),
});

this.myForm.valueChanges.subscribe((formValue) => {
  this.changeDetector.markForCheck();
});

the markForCheck() writen because of using in this declare:

changeDetection: ChangeDetectionStrategy.OnPush
Bartolomeo answered 10/12, 2018 at 5:34 Comment(1)
This is completely different from the question and although helpful people should be aware that your code is about a different type of change. The question asked about data changing that is from OUTSIDE a component and then having your component know and respond to the fact the data has changed. Your answer is talking about detecting changes within the component itself on things like inputs on a form which are LOCAL to the component.Yuma

© 2022 - 2024 — McMap. All rights reserved.