angularangular-materialmat-tableselectionmodel

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


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

Solution

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