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