question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Checkbox column out of sync with view after pagination

See original GitHub issue

Bug, feature request, or proposal:

Mat-table checkbox select doesn’t update the view after pagination.

What is the current behavior?

The SelectionModel holds the “selected” property correctly, but loses view when you go to the next page.

app.component.ts

import {Component, AfterViewInit, ViewChild} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {MatPaginator, MatSort, MatTableDataSource} from '@angular/material';
import {Observable} from 'rxjs/Observable';
import {merge} from 'rxjs/observable/merge';
import {of as observableOf} from 'rxjs/observable/of';
import {catchError} from 'rxjs/operators/catchError';
import {map} from 'rxjs/operators/map';
import {startWith} from 'rxjs/operators/startWith';
import {switchMap} from 'rxjs/operators/switchMap';
import {SelectionModel} from '@angular/cdk/collections';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
  displayedColumns = ['select','id', 'name', 'progress', 'color'];
  exampleDatabase: ExampleHttpDao | null;
  dataSource = new MatTableDataSource();
  selection = new SelectionModel<UserData>(true, []);

  resultsLength = 0;
  isLoadingResults = true;
  isRateLimitReached = false;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;


  constructor(private http: HttpClient) {}

  ngAfterViewInit() {

    this.exampleDatabase = new ExampleHttpDao(this.http);

    // If the user changes the sort order, reset back to the first page.
    this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);

    merge(this.sort.sortChange, this.paginator.page)
      .pipe(
        startWith({}),
        switchMap(() => {
          this.isLoadingResults = true;
          return this.exampleDatabase!.getRepoIssues(
            this.sort.active, this.sort.direction, this.paginator.pageIndex);
        }),
        map(data => {
          // Flip flag to show that loading has finished.
          this.isLoadingResults = false;
          this.isRateLimitReached = false;
          this.resultsLength = data.total_count;

          return data.items;
        }),
        catchError(() => {
          this.isLoadingResults = false;
          // Catch if the GitHub API has reached its rate limit. Return empty data.
          this.isRateLimitReached = true;
          return observableOf([]);
        })
      ).subscribe(data => this.dataSource.data = data);

      this.dataSource!.paginator = this.paginator;
      this.dataSource!.sort = this.sort;
  
  }

  applyFilter(filterValue: string) {
    filterValue = filterValue.trim(); // Remove whitespace
    filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
    this.dataSource.filter = filterValue;
  }

    /** Whether the number of selected elements matches the total number of rows. */
    isAllSelected() {
      const numSelected = this.selection.selected.length;
      const numRows = this.dataSource.data.length;
      return numSelected === numRows;
    }
  
    /** Selects all rows if they are not all selected; otherwise clear selection. */
    masterToggle() {
      this.isAllSelected() ?
          this.selection.clear() :
          this.dataSource.data.forEach(row => this.selection.select(row));
    }
}



export interface UserDataApi {
  data: UserData[];
  total_count: number;
}

export interface UserData {
  id: string;
  name: string;
  progress: string;
  color: string;
}

/** An example database that the data source uses to retrieve data for the table. */
export class ExampleHttpDao {
  constructor(private http: HttpClient) {}

  getRepoIssues(sort: string, order: string, page: number): Observable<UserDataApi> {
    const requestUrl = './assets/data.json';

    return this.http.get<UserDataApi>(requestUrl);
  }
}

app.component.html

<div class="example-header">
  <mat-form-field>
    <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
  </mat-form-field>
</div>

<mat-table #table [dataSource]="dataSource" class="example-table" matSort matSortActive="created" matSortDisableClear matSortDirection="asc">

  <!--- Note that these columns can be defined in any order.
          The actual rendered columns are set as a property on the row definition" -->

    <!-- Checkbox Column -->
    <ng-container matColumnDef="select">
      <mat-header-cell *matHeaderCellDef>
        <mat-checkbox (change)="$event ? masterToggle() : null"
                      [checked]="selection.hasValue() && isAllSelected()"
                      [indeterminate]="selection.hasValue() && !isAllSelected()">
        </mat-checkbox>
      </mat-header-cell>
      <mat-cell *matCellDef="let row">
        <mat-checkbox (click)="$event.stopPropagation()"
                      (change)="$event ? selection.toggle(row) : null"
                      [checked]="selection.isSelected(row)">
        </mat-checkbox>
      </mat-cell>
    </ng-container>

  <!-- Number Column -->
  <ng-container matColumnDef="id">
    <mat-header-cell *matHeaderCellDef mat-sort-header>id</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.id }}</mat-cell>
  </ng-container>

  <!-- Title Column -->
  <ng-container matColumnDef="name">
    <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
  </ng-container>

  <!-- State Column -->
  <ng-container matColumnDef="progress">
    <mat-header-cell *matHeaderCellDef mat-sort-header>Progress</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.progress }}</mat-cell>
  </ng-container>

  <!-- State Column -->
  <ng-container matColumnDef="color">
    <mat-header-cell *matHeaderCellDef mat-sort-header>Color</mat-header-cell>
    <mat-cell *matCellDef="let row">{{ row.color}}</mat-cell>
  </ng-container>


 <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"
             (click)="selection.toggle(row)">
    </mat-row>
  </mat-table>


  <mat-paginator #paginator
  [pageSize]="10"
  [pageSizeOptions]="[5, 10, 20]">
</mat-paginator>

image

image

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:10 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
mihaiborbeacommented, Jan 17, 2019

Thanks @timdao4, Similarly, I managed to achieve same result with less code: Assuming that you have and object with some unique identifier as your selection model type (as @bwronin mantioned SelectionModel uses Set)

// Element has unique '_id' selection: SelectionModel<Element> = new SelectionModel<Element>(true, []);

I only needed to override the isSelected method and leave the SelectionModel to do its logic as usual.

isChecked(row: any): boolean {
    const found = this.selection.selected.find(el => el._id === row._id);
    if (found) {
      return true;
    }
    return false;
 }

Then, in ngOnInit: this.selection.isSelected = this.isChecked.bind(this);

This allows me to keep the template (and the rest of the selection logic) untouched, as you find it in the Material docs:

<mat-checkbox
          (click)="$event.stopPropagation()"
          (change)="$event ? selection.toggle(element) : null"
          [checked]="selection.isSelected(element)"
>
</mat-checkbox>
1reaction
Taodimcommented, Dec 28, 2018

The SelectionModel is using a Set underneath to track what’s been selected. If you’re passing in an Object the Set will only return true if its the same Object in memory, otherwise you’ll get what you’re experiencing now.

When you make a selection on one page it’s storing your UserData Object, then you change pages and go back which creates an entirely new Object so the Set doesn’t see it as having been a selection.

Assuming you can’t pull all data into the browser, track your selections using the identifier for your Object or some primitive value.

Hope this helps.

Wow, your comment definitely helped me figuring out the solution.

Every time mat-table changes page, selection “looses” track of the ‘row’ object. So when you try to select(row), deselect(row), toggle(row) after a page change, things will get messed up.

This will directly link to mat-checkbox’s [check] not recognizing ‘selection.isSelect(row)’ to re-check the box when you go back to the previous page.

What I did is calling different functions during (change) [checked] <mat-checkbox (click)="$event.stopPropagation()" (change) ="$event.checked ? checked(row) : unChecked(row)" [checked] ="isChecked(row)"> </mat-checkbox>

Then on TS side with the functions below. Note that I added a ‘checked’ boolean variable in my object model. checked(row: any){ this.selection.select(row) var found = this.selection.selected.find(x => x.documentId == row.documentId); if (found) found.checked = true; }

unchecked(row: any){ var found = this.selection.selected.find(x => x.documentId == row.documentId); if (found) found.checked = false; this.selection.deselect(found); }//This is very important to deselect ‘found’ instead of ‘row’ since selectionmodel will not be able to find ‘row’ to deselect after page change.

isChecked(row: any) { var found = this.selection.selected.find(x => x.documentId == row.documentId); if (found) return found.checked;

I spent 2 days debugging this, so hopefully this helped.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Grid with pagination and checkbox column, how to select only ...
I have 30 items in the grid and the pagination is set to 10, now you can only see 10 rows, 10 items....
Read more >
angular - Material 2 Table: View not updating checkboxes
I've tried adding a child view and some other changes, but I can't figure it out. Edit: I logged some things and it...
Read more >
[Material] How to maintain checked state of checkbox after ...
I can see that the SelectionModel array is maintaining itself throughout multiple pagination events. But for some reason the template is loading ...
Read more >
Angular Material Data Table - Data Selection - YouTube
This video is part of the Angular Material In Depth Course - https://angular-university.io/course/angular-material-courseCheck out the PDF ...
Read more >
MUI V5: Tables pt 2 (Sorting, Checkboxes, Pagination)
Part 1 for basic table explanation: https://youtu.be/rsl3tzKkolUIn this video we go over Material UI V5's Table component in a lot more ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found