Skip to content

Commit 085d805

Browse files
andrewseguintinayuangao
authored andcommitted
feat(table): allow data input to be array, stream (#9489)
* feat(table): allow data input to be array, stream * comments * fix connect function * fix connect function v2 * fix demo imports
1 parent 3acbf26 commit 085d805

File tree

13 files changed

+568
-116
lines changed

13 files changed

+568
-116
lines changed

src/cdk/table/table-errors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ export function getTableMissingRowDefsError() {
4747
return Error('Missing definitions for header and row, ' +
4848
'cannot determine which columns should be rendered.');
4949
}
50+
51+
/**
52+
* Returns an error to be thrown when the data source does not match the compatible types.
53+
* @docs-private
54+
*/
55+
export function getTableUnknownDataSourceError() {
56+
return Error(`Provided data source did not match an array, Observable, or DataSource`);
57+
}

src/cdk/table/table.spec.ts

Lines changed: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
getTableMissingMatchingRowDefError,
1212
getTableMissingRowDefsError,
1313
getTableMultipleDefaultRowDefsError,
14-
getTableUnknownColumnError
14+
getTableUnknownColumnError,
15+
getTableUnknownDataSourceError
1516
} from './table-errors';
1617
import {CdkHeaderRowDef, CdkRowDef} from './row';
1718
import {CdkColumnDef} from './cell';
@@ -45,6 +46,7 @@ describe('CdkTable', () => {
4546
BooleanRowCdkTableApp,
4647
WrapperCdkTableApp,
4748
OuterTableApp,
49+
CdkTableWithDifferentDataInputsApp,
4850
],
4951
}).compileComponents();
5052
}));
@@ -113,6 +115,138 @@ describe('CdkTable', () => {
113115
});
114116
});
115117

118+
describe('with different data inputs other than data source', () => {
119+
let dataInputFixture: ComponentFixture<CdkTableWithDifferentDataInputsApp>;
120+
let dataInputComponent: CdkTableWithDifferentDataInputsApp;
121+
let dataInputTableElement: HTMLElement;
122+
123+
let baseData: TestData[] = [
124+
{a: 'a_1', b: 'b_1', c: 'c_1'},
125+
{a: 'a_2', b: 'b_2', c: 'c_2'},
126+
{a: 'a_3', b: 'b_3', c: 'c_3'},
127+
];
128+
129+
beforeEach(() => {
130+
dataInputFixture = TestBed.createComponent(CdkTableWithDifferentDataInputsApp);
131+
dataInputComponent = dataInputFixture.componentInstance;
132+
dataInputFixture.detectChanges();
133+
134+
dataInputTableElement = dataInputFixture.nativeElement.querySelector('cdk-table');
135+
});
136+
137+
it('should render with data array input', () => {
138+
const data = baseData.slice();
139+
dataInputComponent.dataSource = data;
140+
dataInputFixture.detectChanges();
141+
142+
const expectedRender = [
143+
['Column A', 'Column B', 'Column C'],
144+
['a_1', 'b_1', 'c_1'],
145+
['a_2', 'b_2', 'c_2'],
146+
['a_3', 'b_3', 'c_3'],
147+
];
148+
expectTableToMatchContent(dataInputTableElement, expectedRender);
149+
150+
// Push data to the array but neglect to tell the table, should be no change
151+
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
152+
153+
expectTableToMatchContent(dataInputTableElement, expectedRender);
154+
155+
// Notify table of the change, expect another row
156+
dataInputComponent.table.renderRows();
157+
dataInputFixture.detectChanges();
158+
159+
expectedRender.push(['a_4', 'b_4', 'c_4']);
160+
expectTableToMatchContent(dataInputTableElement, expectedRender);
161+
162+
// Remove a row and expect the change in rows
163+
data.pop();
164+
dataInputComponent.table.renderRows();
165+
166+
expectedRender.pop();
167+
expectTableToMatchContent(dataInputTableElement, expectedRender);
168+
169+
// Remove the data input entirely and expect no rows - just header.
170+
dataInputComponent.dataSource = null;
171+
dataInputFixture.detectChanges();
172+
173+
expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);
174+
175+
// Add back the data to verify that it renders rows
176+
dataInputComponent.dataSource = data;
177+
dataInputFixture.detectChanges();
178+
179+
expectTableToMatchContent(dataInputTableElement, expectedRender);
180+
});
181+
182+
it('should render with data stream input', () => {
183+
const data = baseData.slice();
184+
const stream = new BehaviorSubject<TestData[]>(data);
185+
dataInputComponent.dataSource = stream;
186+
dataInputFixture.detectChanges();
187+
188+
const expectedRender = [
189+
['Column A', 'Column B', 'Column C'],
190+
['a_1', 'b_1', 'c_1'],
191+
['a_2', 'b_2', 'c_2'],
192+
['a_3', 'b_3', 'c_3'],
193+
];
194+
expectTableToMatchContent(dataInputTableElement, expectedRender);
195+
196+
// Push data to the array and emit the data array on the stream
197+
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
198+
stream.next(data);
199+
dataInputFixture.detectChanges();
200+
201+
expectedRender.push(['a_4', 'b_4', 'c_4']);
202+
expectTableToMatchContent(dataInputTableElement, expectedRender);
203+
204+
// Push data to the array but rather than emitting, call renderRows.
205+
data.push({a: 'a_5', b: 'b_5', c: 'c_5'});
206+
dataInputComponent.table.renderRows();
207+
dataInputFixture.detectChanges();
208+
209+
expectedRender.push(['a_5', 'b_5', 'c_5']);
210+
expectTableToMatchContent(dataInputTableElement, expectedRender);
211+
212+
// Remove a row and expect the change in rows
213+
data.pop();
214+
expectedRender.pop();
215+
stream.next(data);
216+
217+
expectTableToMatchContent(dataInputTableElement, expectedRender);
218+
219+
// Remove the data input entirely and expect no rows - just header.
220+
dataInputComponent.dataSource = null;
221+
dataInputFixture.detectChanges();
222+
223+
expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);
224+
225+
// Add back the data to verify that it renders rows
226+
dataInputComponent.dataSource = stream;
227+
dataInputFixture.detectChanges();
228+
229+
expectTableToMatchContent(dataInputTableElement, expectedRender);
230+
});
231+
232+
it('should throw an error if the data source is not valid', () => {
233+
dataInputComponent.dataSource = {invalid: 'dataSource'};
234+
235+
expect(() => dataInputFixture.detectChanges())
236+
.toThrowError(getTableUnknownDataSourceError().message);
237+
});
238+
239+
it('should throw an error if the data source is not valid', () => {
240+
dataInputComponent.dataSource = undefined;
241+
dataInputFixture.detectChanges();
242+
243+
// Expect the table to render just the header, no rows
244+
expectTableToMatchContent(dataInputTableElement, [
245+
['Column A', 'Column B', 'Column C']
246+
]);
247+
});
248+
});
249+
116250
it('should render cells even if row data is falsy', () => {
117251
const booleanRowCdkTableAppFixture = TestBed.createComponent(BooleanRowCdkTableApp);
118252
const booleanRowCdkTableElement =
@@ -720,6 +854,36 @@ class SimpleCdkTableApp {
720854
@ViewChild(CdkTable) table: CdkTable<TestData>;
721855
}
722856

857+
@Component({
858+
template: `
859+
<cdk-table [dataSource]="dataSource">
860+
<ng-container cdkColumnDef="column_a">
861+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
862+
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
863+
</ng-container>
864+
865+
<ng-container cdkColumnDef="column_b">
866+
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
867+
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
868+
</ng-container>
869+
870+
<ng-container cdkColumnDef="column_c">
871+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
872+
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
873+
</ng-container>
874+
875+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
876+
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
877+
</cdk-table>
878+
`
879+
})
880+
class CdkTableWithDifferentDataInputsApp {
881+
dataSource: DataSource<TestData> | Observable<TestData[]> | TestData[] | any = null;
882+
columnsToRender = ['column_a', 'column_b', 'column_c'];
883+
884+
@ViewChild(CdkTable) table: CdkTable<TestData>;
885+
}
886+
723887
@Component({
724888
template: `
725889
<cdk-table [dataSource]="dataSource">
@@ -1186,6 +1350,9 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
11861350
}
11871351
}
11881352

1353+
// Copy the expected data array to avoid mutating the test's array
1354+
expectedTableContent = expectedTableContent.slice();
1355+
11891356
// Check header cells
11901357
const expectedHeaderContent = expectedTableContent.shift();
11911358
getHeaderCells(tableElement).forEach((cell, index) => {
@@ -1196,7 +1363,13 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
11961363
});
11971364

11981365
// Check data row cells
1199-
getRows(tableElement).forEach((row, rowIndex) => {
1366+
const rows = getRows(tableElement);
1367+
if (rows.length !== expectedTableContent.length) {
1368+
missedExpectations.push(
1369+
`Expected ${expectedTableContent.length} rows but found ${rows.length}`);
1370+
fail(missedExpectations.join('\n'));
1371+
}
1372+
rows.forEach((row, rowIndex) => {
12001373
getCells(row).forEach((cell, cellIndex) => {
12011374
const expected = expectedTableContent.length ?
12021375
expectedTableContent[rowIndex][cellIndex] :

0 commit comments

Comments
 (0)