I have an Angular component using change detection on push. The component has an input that is modified by the component when the route is updated. If I assign the input to a new reference and modify it, the value in the template is not updated. I thought as long as you assigned a new object the change would be detected, but it is not unless I call detectChanges() on the ChangeDetectorRef.
@Component({
selector: 'app-nav-links',
templateUrl: './nav-links.component.html',
styleUrls: ['./nav-links.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NavLinksComponent implements OnInit {
@Input() links: Link[] = [];
private currentPath: string;
constructor(private router: Router,
private route: ActivatedRoute,
private cdr: ChangeDetectorRef) { }
ngOnInit() {
this.router.events.subscribe(() => {
this.updateActiveRoute();
});
}
private updateActiveRoute() {
this.currentPath = this.route.snapshot.children[0].routeConfig.path;
this.links = [...this.links].map(link => {
link.active = link.url === this.currentPath;
return link;
});
// Why is this required if I am already using the spread operator?
this.cdr.detectChanges();
}
}
Change detection occurs whenever a browser event (or Angular event) occurs. In this scenario, change detection is happening. However, the problem is that the originating reference coming from the parent component did not actually change (it did change from the child component's perspective).
In other words, by overwriting the @Input() parameter within the component, you are essentially breaking the binding between the parent component, and the child component's input parameter.
Based on the way change detection works, where references are checked from top-down, its no surprise that the component is not being updated when the reference does not appear to have changed (from the parent component's perspective).
In order to keep the binding in-sync, setup two-way binding with EventEmitter:
export class NavLinksComponent implements OnInit {
@Input() links: Link[] = [];
@Output() linksChange: EventEmitter<Link[]>;
constructor() {
this.linksChange = new EventEmitter<Link[]>();
}
ngOnInit() {
this.router.events.subscribe(() => {
this.updateActiveRoute();
});
}
private updateActiveRoute() {
this.currentPath = this.route.snapshot.children[0].routeConfig.path;
this.links = [...this.links].map(link => {
link.active = link.url === this.currentPath;
return link;
});
// notify the parent component that the reference has changed
this.linksChange.next(this.links);
}
}
In your calling component's template, setup two-way binding, so that when the inner reference is modified, it notifies the parent component to also update its reference:
<app-nav-links [(links)]="links" />
That way, when the references are checked top-down, the change detector would determine that the reference has changed, and trigger change detection correctly for its components (as it should for components using the OnPush strategy).
This isn't a problem for the default change detector, because by default, the change detector would check all bound references, regardless of whether or not the @Input references have changed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With