Search code examples
angularngforangular2-changedetection

Change detection not working for complex objects used in NgFor


I am using angular 7. I have a parent component that gets a list of user models from an api. When the "get" succeeds it is used in conjunction with an ngFor loop to inject each user into a child component using @input that will display the list of users.

When a property on a single user model is updated the child components do not reflect this change. I understand why this is happening. I understand that the change detection will only kick in if a reference in the list is changed. I don't however know how to get around this problem. I have tried implementing the user model array as an observable and using the async pipe, which works in displaying the data but not when changes are made.

In my specific example I have a "selected" property on my user model, I have a "select all" button on my parent component that I'd like to use to change the "selected" property on all of the users in my array and subsequently display this on the UI. Here is a very simple version of my code

export class ParentComponent {
users: UserModel[];

    ngOnInit() {
       this.userService.getUsers().subscribe(users => {
        this.users = users;
       });
    }

    onSelectAll() {
        for (let user of this.users) {
            user.selected  = true;
        }
    }
}

Parent Component has the following in the template:

<tr users-item [index]="i + 1" [user]="user" *ngFor="let user of users; let i = index"></tr>

And this is the child component that uses the user model to display the data:

export class ChildComponent {
    @Input() user: UserModel;

    firstname: string;
    lastname: string;
    selected: boolean;

    ngOnChanges() {
        if (this.user) {
            this.firstname = this.user.firstname;
            this.lastname = this.user.lastname;
            this.selected = this.user.selected;
        }
    }
 }

and the child component template:

<td><input type="checkbox" [checked]="selected"></td>
<td>{{ fullname }}</td>
<td>{{ lastname }}</td>

Solution

  • You could simply stick to an immutability based approach and create shallow copies with updated properties instead of mutating the source objects.

    In your use case, you can do this by changing the implementation of select all:

    onSelectAll() {
        this.users = this.users.map(user => this.mergeUser(user, {selected: true}));
    }
    
    private mergeUser(source: UserModel, updated: Partial<UserModel>): UserModel{
       return {...source, ...updated};
    }