From bd0f7bcd64845801b96fb57a3de42e914da947f6 Mon Sep 17 00:00:00 2001 From: Ed Morales Date: Mon, 8 May 2017 16:58:56 -0700 Subject: [PATCH] feat(data-table): indeterminate state in 'selectAll' checkbox (closes #571) (#573) * feat(data-table): indeterminate state in 'selectAll' checkbox when atleast one row is selected and not every row is selected, the checkbox for select/deselect all should be in indeterminate state. * fix(data-table): set variable when selecting all instead of recalculating --- .../core/data-table/data-table.component.html | 3 +- .../data-table/data-table.component.spec.ts | 149 ++++++++++++++++-- .../core/data-table/data-table.component.ts | 64 ++++++-- 3 files changed, 192 insertions(+), 24 deletions(-) diff --git a/src/platform/core/data-table/data-table.component.html b/src/platform/core/data-table/data-table.component.html index 0a64214776..504573660d 100644 --- a/src/platform/core/data-table/data-table.component.html +++ b/src/platform/core/data-table/data-table.component.html @@ -6,7 +6,8 @@ #checkBoxAll *ngIf="isMultiple" [disabled]="!hasData" - [checked]="areAllSelected() && hasData" + [indeterminate]="indeterminate && !allSelected && hasData" + [checked]="allSelected && hasData" (click)="selectAll(!checkBoxAll.checked)"> diff --git a/src/platform/core/data-table/data-table.component.spec.ts b/src/platform/core/data-table/data-table.component.spec.ts index 42166d9fc3..feb7936bfd 100644 --- a/src/platform/core/data-table/data-table.component.spec.ts +++ b/src/platform/core/data-table/data-table.component.spec.ts @@ -8,10 +8,12 @@ import 'hammerjs'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { TdDataTableColumnComponent } from './data-table-column/data-table-column.component'; +import { TdDataTableRowComponent } from './data-table-row/data-table-row.component'; import { TdDataTableComponent, ITdDataTableColumn } from './data-table.component'; import { TdDataTableService } from './services/data-table.service'; import { CovalentDataTableModule } from './data-table.module'; import { NgModule, DebugElement } from '@angular/core'; +import { MdCheckbox } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('Component: DataTable', () => { @@ -120,22 +122,141 @@ describe('Component: DataTable', () => { })(); }); - it('should not set the data input and not fail when selectable and multiple', (done: DoneFn) => { - inject([], () => { - let fixture: ComponentFixture = TestBed.createComponent(TdDataTableSelectableTestComponent); - let element: DebugElement = fixture.debugElement; - let component: TdDataTableSelectableTestComponent = fixture.debugElement.componentInstance; - - component.selectable = true; - component.multiple = true; + describe('selectable and multiple', () => { + + it('should not set the data input and not fail', (done: DoneFn) => { + inject([], () => { + let fixture: ComponentFixture = TestBed.createComponent(TdDataTableSelectableTestComponent); + let element: DebugElement = fixture.debugElement; + let component: TdDataTableSelectableTestComponent = fixture.debugElement.componentInstance; + + component.selectable = true; + component.multiple = true; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + // if it finishes in means it didnt fail + done(); + }); + })(); + }); + + it('should select one and be in indeterminate state, select all and then unselect all', + (done: DoneFn) => { inject([], () => { + let fixture: ComponentFixture = TestBed.createComponent(TdDataTableSelectableTestComponent); + let element: DebugElement = fixture.debugElement; + let component: TdDataTableSelectableTestComponent = fixture.debugElement.componentInstance; + + component.selectable = true; + component.multiple = true; + component.columns = [ + { name: 'sku', label: 'SKU #' }, + { name: 'item', label: 'Item name' }, + { name: 'price', label: 'Price (US$)', numeric: true }, + ]; + + component.data = [{ sku: '1452-2', item: 'Pork Chops', price: 32.11 }, + { sku: '1421-0', item: 'Prime Rib', price: 41.15 }, + { sku: '1452-1', item: 'Sirlone', price: 22.11 }, + { sku: '1421-3', item: 'T-Bone', price: 51.15 }]; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + let dataTableComponent: TdDataTableComponent = fixture.debugElement.query(By.directive(TdDataTableComponent)).componentInstance; + // check how many rows (without counting the columns) were rendered + expect(fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent)).length - 1).toBe(4); + // check to see checkboxes states + expect(dataTableComponent.indeterminate).toBeFalsy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + // select a row with a click event + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its in indeterminate state + expect(dataTableComponent.indeterminate).toBeTruthy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + // select the rest of the rows by clicking in selectAll + fixture.debugElement.query(By.directive(MdCheckbox)).triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its in indeterminate state and allSelected + expect(dataTableComponent.indeterminate).toBeTruthy(); + expect(dataTableComponent.allSelected).toBeTruthy(); + // unselect all rows by clicking in unselect all + fixture.debugElement.query(By.directive(MdCheckbox)).triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its not in indeterminate state and not allSelected + expect(dataTableComponent.indeterminate).toBeFalsy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + done(); + }); + }); + }); + }); + })(); + }); + + it('should be interminate when atleast one row is selected and allSelected when all rows are selected', + (done: DoneFn) => { inject([], () => { + let fixture: ComponentFixture = TestBed.createComponent(TdDataTableSelectableTestComponent); + let element: DebugElement = fixture.debugElement; + let component: TdDataTableSelectableTestComponent = fixture.debugElement.componentInstance; + + component.selectable = true; + component.multiple = true; + component.columns = [ + { name: 'sku', label: 'SKU #' }, + { name: 'item', label: 'Item name' }, + { name: 'price', label: 'Price (US$)', numeric: true }, + ]; + + component.data = [{ sku: '1452-2', item: 'Pork Chops', price: 32.11 }, + { sku: '1421-0', item: 'Prime Rib', price: 41.15 }, + { sku: '1452-1', item: 'Sirlone', price: 22.11 }, + { sku: '1421-3', item: 'T-Bone', price: 51.15 }]; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + let dataTableComponent: TdDataTableComponent = fixture.debugElement.query(By.directive(TdDataTableComponent)).componentInstance; + // check how many rows (without counting the columns) were rendered + expect(fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent)).length - 1).toBe(4); + // check to see checkboxes states + expect(dataTableComponent.indeterminate).toBeFalsy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + // select a row with a click event + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its in indeterminate state + expect(dataTableComponent.indeterminate).toBeTruthy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + // select the rest of the rows + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[1].triggerEventHandler('click', new Event('click')); + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[3].triggerEventHandler('click', new Event('click')); + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[4].triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its in indeterminate state and allSelected + expect(dataTableComponent.indeterminate).toBeTruthy(); + expect(dataTableComponent.allSelected).toBeTruthy(); + // unselect one of the rows + fixture.debugElement.queryAll(By.directive(TdDataTableRowComponent))[2].triggerEventHandler('click', new Event('click')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + // check to see if its in indeterminate state and not allSelected + expect(dataTableComponent.indeterminate).toBeTruthy(); + expect(dataTableComponent.allSelected).toBeFalsy(); + done(); + }); + }); + }); + }); + })(); + }); - fixture.detectChanges(); - fixture.whenStable().then(() => { - // if it finishes in means it didnt fail - done(); - }); - })(); }); + }); @Component({ diff --git a/src/platform/core/data-table/data-table.component.ts b/src/platform/core/data-table/data-table.component.ts index addfa8b481..0410ecf6a5 100644 --- a/src/platform/core/data-table/data-table.component.ts +++ b/src/platform/core/data-table/data-table.component.ts @@ -63,6 +63,8 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI private _columns: ITdDataTableColumn[]; private _selectable: boolean = false; private _multiple: boolean = true; + private _allSelected: boolean = false; + private _indeterminate: boolean = false; /** sorting */ private _sortable: boolean = false; @@ -73,6 +75,21 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI private _templateMap: Map> = new Map>(); @ContentChildren(TdDataTableTemplateDirective) _templates: QueryList; + /** + * Returns true if all values are selected. + */ + get allSelected(): boolean { + return this._allSelected; + } + + /** + * Returns true if all values are not deselected + * and atleast one is. + */ + get indeterminate(): boolean { + return this._indeterminate; + } + /** * Implemented as part of ControlValueAccessor. */ @@ -277,18 +294,10 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI * Refreshes data table and rerenders [data] and [columns] */ refresh(): void { + this._calculateCheckboxState(); this._changeDetectorRef.markForCheck(); } - /** - * Checks if all visible rows are selected. - */ - areAllSelected(): boolean { - const match: string = - this._data ? this._data.find((d: any) => !this.isRowSelected(d)) : true; - return typeof match === 'undefined'; - } - /** * Selects or clears all rows depending on 'checked' value. */ @@ -300,8 +309,12 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI this._value.push(row); } }); + this._allSelected = true; + this._indeterminate = true; } else { this.clearModel(); + this._allSelected = false; + this._indeterminate = false; } this.onSelectAll.emit({rows: this._value, selected: checked}); } @@ -343,6 +356,7 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI this._value.splice(index, 1); } } + this._calculateCheckboxState(); this.onRowSelect.emit({row: row, selected: checked}); this.onChange(this._value); } @@ -391,4 +405,36 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI } } + /** + * Calculate all the state of all checkboxes + */ + private _calculateCheckboxState(): void { + this._calculateAllSelected(); + this._calculateIndeterminate(); + } + + /** + * Checks if all visible rows are selected. + */ + private _calculateAllSelected(): void { + const match: string = + this._data ? this._data.find((d: any) => !this.isRowSelected(d)) : true; + this._allSelected = typeof match === 'undefined'; + } + + /** + * Checks if all visible rows are selected. + */ + private _calculateIndeterminate(): void { + this._indeterminate = false; + if (this._data) { + for (let row of this._data) { + if (!this.isRowSelected(row)) { + continue; + } + this._indeterminate = true; + } + } + } + }