Is it possible to set the CompareWith function of Angular SelectionModel object in TypeScript
Asked Answered
G

3

5

I have a component that renders mat-table in it's template. I want to pre-select some of the rows. The SelectionModel I have contains objects representing each selected item (not simple strings or numbers) and the method for comparing these is more complex than the native SelectionModel's method.

If this was a mat-select form control, I could use the [compareWith] directive to supply a custom comparison function e.g.

<mat-select [compareWith]="myCompareFunction"  >...

but this is not suitable solution - as I need a tabular presentation. I an following closely the example in the Angular documents. The mat-table examples here: have a mat-table with selection checkbox on each row and this is the approach I have followed.

In the example's Component code it uses a SelectionModel object.

import {SelectionModel} from '@angular/cdk/collections';
....
....
selection = new SelectionModel<PeriodicElement>(true, []);

I am searching for a way to supply a custom comparison function to the SelectionModel object. Can SelectionModel be sub-classed with an override for the function or can a method be 'injected' in some way?

I have tried to sub-class SelectionModel and declare a new compareWith function, but this doesn't seem to be what's required. Can anyone advise?

   import { SelectionModel } from '@angular/cdk/collections';
   import { InputOptionIf } from '../formosa-interfaces/dynamic-field-config-if';

   export class ModalSelectSelectionModel extends SelectionModel<InputOptionIf>{
      compareWith(o1:any,o2:any) {
        console.log("ModalSelectSelectionModel.compareWith()")
        return( <InputOptionIf>o1.label==<InputOptionIf>o2.label);
      }
   }  
Greasy answered 1/7, 2020 at 23:3 Comment(2)
The selection model does not support custom comparison. Please refer to the code You can manually toggle those rows when you are to display the table, so those are selected by default.Expectoration
Thanks for this feedback, and the link which is very useful.Greasy
M
3

If you are stuck on angular <14 you can extend SelectionModel and override the isSelected method. In the CDK implementation SelectionModel uses a Set to store the selection and they check if an item is contained in that set by using Set.has. Thankfully the only place where Set.has is called is in isSelected. Everywhere else in the class uses isSelected to check if the item already exists.

You should also override the deselect method to first find the value in the selection using the compareWith function. This ensures that the value being deselected is the same instance as the value in the set. Without this some values may not get deselected as expected.

Here is an example implementation which accepts a compareWith function in the same way the angular 14 implementation works. When you upgrade to angular 14 you should be able to simply replace ComparableSelectionModel with SelectionModel.

export class ComparableSelectionModel<T> extends SelectionModel<T> {
  private compareWith: (o1: T, o2: T) => boolean;

  constructor(
    _multiple?: boolean, 
    initial?: T[], 
    _emitChanges?: boolean, 
    compareWith?: (o1: T, o2: T) => boolean) {
    super(_multiple, initial, _emitChanges);

    this.compareWith = compareWith ? compareWith : (o1, o2) => o1 === o2;
  }

  override isSelected(value: T): boolean {
    return this.selected.some((x) => this.compareWith(value, x);
  }

  /**
   * We also need to override deselect since you may have objects that 
   * meet the comparison criteria but are not the same instance.
   */
  override deselect(...values: T[]): void {
    // using bracket notation here to work around private methods
    this['_verifyValueAssignment'](values);

    values.forEach((value) => {
      // need to find the exact object in the selection set so it 
      // actually gets deleted
      const found = this.selected.find((x) => this.compareWith(value, x);
      if (found) {
        this['_unmarkSelected'](found);
      }
    });

    this['_emitChangeEvent']();
  }
}

Monogram answered 16/12, 2022 at 21:20 Comment(1)
To be super sure you can also just copy the source-code of the new version from here. Just make sure to change the Source property type of the SelectionChange interface to your ClassName and remove the ngDevMode type checkFebrile
C
2

As of Angular 14, SelectionModel now supports custom compareWith function. https://material.angular.io/cdk/collections/api#SelectionModel https://vscode.dev/github/angular/components/blob/a310fefb907cf88f38f46e0614289a4b057e8af0/src/cdk/collections/selection-model.ts#L43

interface IFoo {
 id: string;
 bar: string;
} 

selectionModel = new SelectionModel<IFoo>(
        true,
        [],
        true,
        (c1: IFoo , c2: IFoo ) => c1.id === c2.id
    );
Cubism answered 11/10, 2022 at 19:28 Comment(0)
G
1

So, having examined the source, it doesn't seem possible to sub-class the SelectionModel to replace the method used for comparing objects. This is because most of the properties of the SelectionModel are declared as private in the TypeScript. I suppose that this could all be ignored & overridden since it's basically JS underneath, but the compiler complains when I tried to work with them - and I like keep a clean compilation.

What I have done is create a new class without any inheritance form SelectionModel. This class implements my required subset of SelectionModel (toggling items in the selection, and emitting events when changes happen) - with the capability I needed to supply a function for comparing objects, and use this one instead of the Collections/SelectionModel class. The cast is a bit messy, but it prevents the compiler moaning!

this.selectionModel= <SelectionModel<InputOptionIf>><unknown>new MySelectionMode(...)

Its working now, and that's all I needed. Thanks for the pointers.

Greasy answered 2/7, 2020 at 14:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.