4

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();
    }
}

2 Answers 2

6

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the detailed explanation, now I finally understand how it works. Just one question, is it better or worse to use event emitter VS calling detectChanges on ChangeDetectorRef?
detectChanges() also works. But it usually implies a lack of understanding, and it could result in unintended side-effects from the two models being out of sync. The right way to handle this is through two-way binding.
0

The problem is change detection on nested objects, or rather the lack of it. One of the simplest solutions is to JSON.parse(JSON.stringify()) your object before setting the links and path:

    this.currentPath = JSON.parse(JSON.stringify(this.route.snapshot.children[0].routeConfig.path));

    this.links = JSON.parse(JSON.stringify([...this.links].map(link => {
        link.active = link.url === this.currentPath;
        return link;
    })));

Which is a top level change of your object, and automatically proc's a change. The other option is manually proc with ChangeDetectorRef, which does a full object scan and notices the changes (as you have implemented).

Basic idea: Nested objects are not within the scope of the change detector, so only changes to the entire object are noticed (and not when only the nested portions are changed).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.