Skip to content

Commit

Permalink
feat(table): allow data input to be array, stream
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewseguin committed Jan 30, 2018
1 parent a48f75b commit 018735f
Show file tree
Hide file tree
Showing 13 changed files with 563 additions and 116 deletions.
8 changes: 8 additions & 0 deletions src/cdk/table/table-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ export function getTableMissingRowDefsError() {
return Error('Missing definitions for header and row, ' +
'cannot determine which columns should be rendered.');
}

/**
* Returns an error to be thrown when the data source does not match the compatible types.
* @docs-private
*/
export function getTableUnknownDataSourceError() {
return Error(`Provided data source did not match an array, Observable, or DataSource`);
}
177 changes: 175 additions & 2 deletions src/cdk/table/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
getTableMissingMatchingRowDefError,
getTableMissingRowDefsError,
getTableMultipleDefaultRowDefsError,
getTableUnknownColumnError
getTableUnknownColumnError,
getTableUnknownDataSourceError
} from './table-errors';
import {CdkHeaderRowDef, CdkRowDef} from './row';
import {CdkColumnDef} from './cell';
Expand Down Expand Up @@ -45,6 +46,7 @@ describe('CdkTable', () => {
BooleanRowCdkTableApp,
WrapperCdkTableApp,
OuterTableApp,
CdkTableWithDifferentDataInputsApp,
],
}).compileComponents();
}));
Expand Down Expand Up @@ -113,6 +115,138 @@ describe('CdkTable', () => {
});
});

describe('with different data inputs other than data source', () => {
let dataInputFixture: ComponentFixture<CdkTableWithDifferentDataInputsApp>;
let dataInputComponent: CdkTableWithDifferentDataInputsApp;
let dataInputTableElement: HTMLElement;

let baseData: TestData[] = [
{a: 'a_1', b: 'b_1', c: 'c_1'},
{a: 'a_2', b: 'b_2', c: 'c_2'},
{a: 'a_3', b: 'b_3', c: 'c_3'},
];

beforeEach(() => {
dataInputFixture = TestBed.createComponent(CdkTableWithDifferentDataInputsApp);
dataInputComponent = dataInputFixture.componentInstance;
dataInputFixture.detectChanges();

dataInputTableElement = dataInputFixture.nativeElement.querySelector('cdk-table');
});

it('should render with data array input', () => {
const data = baseData.slice();
dataInputComponent.dataSource = data;
dataInputFixture.detectChanges();

const expectedRender = [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
];
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Push data to the array but neglect to tell the table, should be no change
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});

expectTableToMatchContent(dataInputTableElement, expectedRender);

// Notify table of the change, expect another row
dataInputComponent.table.renderRows();
dataInputFixture.detectChanges();

expectedRender.push(['a_4', 'b_4', 'c_4']);
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Remove a row and expect the change in rows
data.pop();
dataInputComponent.table.renderRows();

expectedRender.pop();
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Remove the data input entirely and expect no rows - just header.
dataInputComponent.dataSource = null;
dataInputFixture.detectChanges();

expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);

// Add back the data to verify that it renders rows
dataInputComponent.dataSource = data;
dataInputFixture.detectChanges();

expectTableToMatchContent(dataInputTableElement, expectedRender);
});

it('should render with data stream input', () => {
const data = baseData.slice();
const stream = new BehaviorSubject<TestData[]>(data);
dataInputComponent.dataSource = stream;
dataInputFixture.detectChanges();

const expectedRender = [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
];
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Push data to the array and emit the data array on the stream
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
stream.next(data);
dataInputFixture.detectChanges();

expectedRender.push(['a_4', 'b_4', 'c_4']);
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Push data to the array but rather than emitting, call renderRows.
data.push({a: 'a_5', b: 'b_5', c: 'c_5'});
dataInputComponent.table.renderRows();
dataInputFixture.detectChanges();

expectedRender.push(['a_5', 'b_5', 'c_5']);
expectTableToMatchContent(dataInputTableElement, expectedRender);

// Remove a row and expect the change in rows
data.pop();
expectedRender.pop();
stream.next(data);

expectTableToMatchContent(dataInputTableElement, expectedRender);

// Remove the data input entirely and expect no rows - just header.
dataInputComponent.dataSource = null;
dataInputFixture.detectChanges();

expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);

// Add back the data to verify that it renders rows
dataInputComponent.dataSource = stream;
dataInputFixture.detectChanges();

expectTableToMatchContent(dataInputTableElement, expectedRender);
});

it('should throw an error if the data source is not valid', () => {
dataInputComponent.dataSource = {invalid: 'dataSource'};

expect(() => dataInputFixture.detectChanges())
.toThrowError(getTableUnknownDataSourceError().message);
});

it('should throw an error if the data source is not valid', () => {
dataInputComponent.dataSource = undefined;
dataInputFixture.detectChanges();

// Expect the table to render just the header, no rows
expectTableToMatchContent(dataInputTableElement, [
['Column A', 'Column B', 'Column C']
]);
});
});

it('should render cells even if row data is falsy', () => {
const booleanRowCdkTableAppFixture = TestBed.createComponent(BooleanRowCdkTableApp);
const booleanRowCdkTableElement =
Expand Down Expand Up @@ -720,6 +854,36 @@ class SimpleCdkTableApp {
@ViewChild(CdkTable) table: CdkTable<TestData>;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource">
<ng-container cdkColumnDef="column_a">
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_b">
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_c">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
</cdk-table>
`
})
class CdkTableWithDifferentDataInputsApp {
dataSource: DataSource<TestData> | Observable<TestData[]> | TestData[] | any = null;
columnsToRender = ['column_a', 'column_b', 'column_c'];

@ViewChild(CdkTable) table: CdkTable<TestData>;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource">
Expand Down Expand Up @@ -1186,6 +1350,9 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
}
}

// Copy the expected data array to avoid mutating the test's array
expectedTableContent = expectedTableContent.slice();

// Check header cells
const expectedHeaderContent = expectedTableContent.shift();
getHeaderCells(tableElement).forEach((cell, index) => {
Expand All @@ -1196,7 +1363,13 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
});

// Check data row cells
getRows(tableElement).forEach((row, rowIndex) => {
const rows = getRows(tableElement);
if (rows.length !== expectedTableContent.length) {
missedExpectations.push(
`Expected ${expectedTableContent.length} rows but found ${rows.length}`);
fail(missedExpectations.join('\n'));
}
rows.forEach((row, rowIndex) => {
getCells(row).forEach((cell, cellIndex) => {
const expected = expectedTableContent.length ?
expectedTableContent[rowIndex][cellIndex] :
Expand Down
Loading

0 comments on commit 018735f

Please sign in to comment.