angularangular-cdkselectionmodel

How does angular's selection model determine equality of two objects?


I'm trying to select pre-select some users in a table using Angular's selectionmodel. The call retrieving the users in the table and the call retrieving the already selected users are different so the actual objects are not the same.

I tried writing an equals method on the UserProfile class, this does not seem to change anything. Rewriting the code to use id's would fix the problem but i would like to have the selection model handling the actual objects instead of id's.

This is the code i'm using, but i hope my question is clear enough.

@Input() selected: UserProfile[];

ngOnInit() {
    this.selection = new SelectionModel<UserProfile>(true, this.selected);

Solution

  • The SelectionModel is implemented as a part of @angular/cdk library. The documentation can be found in the Collections page from Angular Material Documentation.

    In the code we use the following import:

    import { SelectionModel } from '@angular/cdk/collections';
    

    The SelectionModel is build using the native JavascriptSet() object, as can be found in the source code:

    private _selection = new Set<T>();
    

    The Set object lets you store unique values of any type, whether primitive values or object references.

    The implementation for SelectionModel checks if an item is contained in that set by using Set.has method.


    What we need to consider first is that, in Javascript, two different instances of objects are always not equal:

    const obj1 = { a: 1, b: 2 };
    const obj2 = { a: 1, b: 2 };
    obj1 !== obj2; // => true
    

    So, the following situation will arive:

    const mySet = new Set();
    const obj1 = {a: 1, b: 2};
    const obj2 = {a: 1, b: 2};
    
    mySet.add(obj1);
    
    mySet.has(obj1); // -> Output: true
    mySet.has({a: 1, b: 2}); // -> Output: false
    mySet.has(obj2); // -> Output: false
    
    mySet.add(obj2)   // obj2 is referencing a different object, so the object will be added in the set
    
    mySet.size; // -> Output: 2
    
    mySet.forEach((value) => {
      console.log(value);
    });
    // Output: {a: 1, b: 2}
    // Output: {a: 1, b: 2}
    

    More info about JS Set() here.


    UPDATE (27.03.2023) - Angular 14+

    From Angular 14, the SelectionModel supports a custom compareWith function - a function optionally passed in order to customize how the Selection Model uniquely identifies the items:

    compareWith: (o1: T, o2: T) => boolean
    

    https://material.angular.io/cdk/collections/api#SelectionModel

    So, we can compare our objects in the selection model by using their attributes (eg: name attribute). The compare functions returns a boolean:

    interface IPeriodicElement {
      name: string;
      symbol: string;
      ...
    }
    
    customCompareFn = (o1: IPeriodicElement, o2: IPeriodicElement) =>
        o1.name === o2.name;
    

    Initialize our SelectionModel as follows:

    public selection = new SelectionModel<IPeriodicElement>(
        true,
        this.initialSelection,
        true,
        (o1: IPeriodicElement, o2: IPeriodicElement) =>
        o1.name === o2.name;
      );
    

    Stackblitz: https://stackblitz.com/edit/angular-ntmyut?file=src/app/selection-example/selection-example.component.ts


    For Angular <=13:

    There is no method to override the comparison method in the Set object, so I've wrote my own implementation of the SelectionModel - SelectionModelImmutableJS by using the very popular library called immutable-js. The implementation was inspired by the following answer.

    To simplify, by using immutable-js, we'll have the following situation:

    const { Map, Set } = require('immutable');
    const map1 = Map({ a: 1, b: 2 });
    const map2 = Map({ a: 1, b: 2 });
    
    map1 !== map2; // => true, two different instances are not reference-equal
    map1.equals(map2); // true, but are value-equal if they have the same values
    const set = Set().add(map1);
    set.has(map2); // true, because these are value-equal
    

    The code for the Selection Model is a little bit too large and I will not post it inline - it can be found in the working demo.

    In the app we'll use:

    import { SelectionModelImmutableJS } from './selection-model-immutable';
    
    .....
    
    
    public selection = new SelectionModelImmutableJS<IPeriodicElement>(
        true,
        this.initialSelection
      );
    

    The full working demo: https://stackblitz.com/edit/angular-ivy-wnvohl