Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data-table): indeterminate state in 'selectAll' checkbox (closes #571) #573

Merged
merged 2 commits into from
May 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/platform/core/data-table/data-table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
#checkBoxAll
*ngIf="isMultiple"
[disabled]="!hasData"
[checked]="areAllSelected() && hasData"
[indeterminate]="indeterminate && !allSelected && hasData"
[checked]="allSelected && hasData"
(click)="selectAll(!checkBoxAll.checked)">
</md-checkbox>
</th>
Expand Down
149 changes: 135 additions & 14 deletions src/platform/core/data-table/data-table.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<any> = 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<any> = 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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice unit test 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍷 🎩

(done: DoneFn) => { inject([], () => {
let fixture: ComponentFixture<any> = 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<any> = 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({
Expand Down
64 changes: 55 additions & 9 deletions src/platform/core/data-table/data-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -73,6 +75,21 @@ export class TdDataTableComponent implements ControlValueAccessor, AfterContentI
private _templateMap: Map<string, TemplateRef<any>> = new Map<string, TemplateRef<any>>();
@ContentChildren(TdDataTableTemplateDirective) _templates: QueryList<TdDataTableTemplateDirective>;

/**
* 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.
*/
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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});
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
}
}

}