14
votes

The Angular Material documentation gives a nice example for how to add selection to a table (Table Selection docs). They even provide a Stackblitz to try it out.

I found in the code for the SelectionModel constructor that the first argument is whether there can be multiple selections made (true) or not (false). The second argument is an array of initially selected values.

In the demo, they don't have any initially selected values, so the second argument in their constructor (line 36) is an empty array ([]).

I want to change it so that there is an initially selected value, so I changed line 36 to:

selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);

This changes the checkbox in the header to an indeterminate state (as expected), but does not cause the row in the table to be selected. Am I setting the initial value incorrectly, or what am I missing here? How can I set an initially selected value?

5

5 Answers

20
votes

Tricky one. You need to initialize the selection by extracting that particular PeriodicElement object from your dataSource input, and passing it to the constructor.

In this particular case, you could code

selection = new SelectionModel<PeriodicElement>(true, [this.dataSource.data[1]);

It's because of the way SelectionModel checks for active selections.

In your table markup you have

<mat-checkbox ... [checked]="selection.isSelected(row)"></mat-checkbox>

You expect this binding to mark the corresponding row as checked. But the method isSelected(row) won't recognize the object passed in here as being selected, because this is not the object your selection received in its constructor.

"row" points to an object from the actual MatTableDataSource input:

dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);

But the selection initialization:

selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);

happens with a new object you create on the fly. Your selection remembers THIS object as a selected one.

When angular evaluates the bindings in the markup, SelectionModel internally checks for object identity. It's going to look for the object that "row" points to in the internal set of selected objects.

Compare to lines 99-101 and 16 from the SelectionModel source code:

isSelected(value: T): boolean {
  return this._selection.has(value);
}

and

private _selection = new Set<T>();
3
votes

I was facing the same issue, I used dataSource to set the initial value manually in ngOnInit()

ngOnInit() {
    this.dataSource.data.forEach(row => {
      if (row.symbol == "H") this.selection.select(row);
    });
  }
2
votes

If you do the following, it works too

selection = new SelectionModel<PeriodicElement>(true, [ELEMENT_DATA[1]])

To select all you can do

selection = new SelectionModel<PeriodicElement>(true, [...ELEMENT_DATA])

I hope the answer is helpful

0
votes

Or more dynamically if you have a set of values and you want to filter them before:

selection = new SelectionModel<PeriodicElement>(true, [
...this.dataSource.data.filter(row => row.weight >= 4.0026)
]);
0
votes

This gets more tricky if you have data loading asynchronously from an api. Here is how I did it: Firstly I have implemented the DataSource from "@angular/cdk/table". I also have an RxJS Subject that fires whenever data is loaded (first time or when user changes page in the pagination section)

export abstract class BaseTableDataSource<T> implements DataSource<T>{
    private dataSubject = new BehaviorSubject<T[]>([]);
    private loadingSubject = new BehaviorSubject<boolean>(false);
    private totalRecordsSubject = new BehaviorSubject<number>(null);

    public loading$ = this.loadingSubject.asObservable();
    public dataLoaded$ = this.dataSubject.asObservable();
    public totalRecords$ = this.totalRecordsSubject.asObservable().pipe(filter(v => v != null));
    
    constructor(){}

    connect(collectionViewer: CollectionViewer): Observable<T[]>{
        return this.dataSubject.asObservable();
    }

    disconnect(collectionViewer: CollectionViewer): void {
        this.dataSubject.complete();
        this.loadingSubject.complete();
        this.totalRecordsSubject.complete();
    }
    
    abstract fetchData(pageIndex, pageSize, ...params:any[]) : Observable<TableData<T>>;
    abstract columnMetadata(): {[colName: string]: ColMetadataDescriptor };

    loadData(pageIndex, pageSize, params?:any[]): void{
        this.loadingSubject.next(true);

        this.fetchData(pageIndex, pageSize, params).pipe(
            finalize(() => this.loadingSubject.next(false))
        )
        .subscribe(data => {
            this.totalRecordsSubject.next(data.totalNumberOfRecords);
            this.dataSubject.next(data.records)
        });
    }

}

Now when I want to pre-select a row, I can write a function like this in my component which hosts a table that uses an implementation of the above mentioned data source

  selectRow(rowSelectionFn: (key: string) => boolean){
    this.dataSource.dataLoaded$.pipe(takeUntil(this.destroyed$))
    .subscribe(data => {
      const foundRecord = data.filter(rec => rowSelectionFn(rec));
      if(foundRecord && foundRecord.length >= 0){
        this.selection.toggle(foundRecord[0]);
      }
    });
  }