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?
Actually, there are two ways of detecting and acting upon when an input changes in the child component in angular2+ :
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
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.
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. 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.
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.
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?
ngOnChanges()
isn't called when a nested object has changed. Maybe you find a solution here
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);
}
SimpleChanges
interface at the top of your file?
<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 object
ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
@Input() set categoryId(categoryId: number) {
console.log(categoryId)
}
please try using this method. Hope this helps
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.
@Input
parameter does not detect changes in complex nested object type but it does in simple native objects.
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
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.
Here ngOnChanges will trigger always when your input property changes:
ngOnChanges(changes: SimpleChanges): void {
console.log(changes.categoryId.currentValue)
}
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
});
}
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>
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()
Example usage:
@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');
Utility function:
export function observeProperty<T, K extends keyof T>(target: T, key: K) {
const subject = new BehaviorSubject<T[K]>(target[key]);
Object.defineProperty(target, key, {
get(): T[K] { return subject.getValue(); },
set(newValue: T[K]): void {
if (newValue !== subject.getValue()) {
subject.next(newValue);
}
}
});
return subject;
}
subject
will always emit.
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;
}
}
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();
}
You can use the ngOnChanges() lifecycle method
@Input() inputValue: string;
ngOnChanges(changes: SimpleChanges) {
console.log(changes['inputValue'].currentValue);
}
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.
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
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;
Success story sharing
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.