sass-references/angular-material/material/table/table.spec.ts

1185 lines
39 KiB
TypeScript

import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {
waitForAsync,
ComponentFixture,
fakeAsync,
flushMicrotasks,
TestBed,
tick,
} from '@angular/core/testing';
import {MatTable, MatTableDataSource, MatTableModule} from './index';
import {DataSource} from '@angular/cdk/table';
import {BehaviorSubject, Observable} from 'rxjs';
import {MatSort, MatSortHeader, MatSortModule} from '@angular/material/sort';
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
describe('MatTable', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
MatTableModule,
MatPaginatorModule,
MatSortModule,
NoopAnimationsModule,
MatTableApp,
MatTableWithWhenRowApp,
ArrayDataSourceMatTableApp,
NativeHtmlTableApp,
MatTableWithSortApp,
MatTableWithPaginatorApp,
StickyTableApp,
TableWithNgContainerRow,
NestedTableApp,
MatFlexTableApp,
],
});
}));
describe('with basic data source', () => {
it('should be able to create a table with the right content and without when row', () => {
let fixture = TestBed.createComponent(MatTableApp);
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table')!;
const data = fixture.componentInstance.dataSource!.data;
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[data[0].a, data[0].b, data[0].c],
[data[1].a, data[1].b, data[1].c],
[data[2].a, data[2].b, data[2].c],
['fourth_row'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should create a table with special when row', () => {
let fixture = TestBed.createComponent(MatTableWithWhenRowApp);
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table');
expectTableToMatchContent(tableElement, [
['Column A'],
['a_1'],
['a_2'],
['a_3'],
['fourth_row'],
['Footer A'],
]);
});
it('should create a table with multiTemplateDataRows true', () => {
let fixture = TestBed.createComponent(MatTableWithWhenRowApp);
fixture.componentInstance.multiTemplateDataRows = true;
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table');
expectTableToMatchContent(tableElement, [
['Column A'],
['a_1'],
['a_2'],
['a_3'],
['a_4'], // With multiple rows, this row shows up along with the special 'when' fourth_row
['fourth_row'],
['Footer A'],
]);
});
it('should be able to render a table correctly with native elements', () => {
let fixture = TestBed.createComponent(NativeHtmlTableApp);
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table');
const data = fixture.componentInstance.dataSource!.data;
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[data[0].a, data[0].b, data[0].c],
[data[1].a, data[1].b, data[1].c],
[data[2].a, data[2].b, data[2].c],
[data[3].a, data[3].b, data[3].c],
]);
});
it('should be able to nest tables', () => {
const fixture = TestBed.createComponent(NestedTableApp);
fixture.detectChanges();
const outerTable = fixture.nativeElement.querySelector('table');
const innerTable = outerTable.querySelector('table');
const outerRows = Array.from<HTMLTableRowElement>(outerTable.querySelector('tbody').rows);
const innerRows = Array.from<HTMLTableRowElement>(innerTable.querySelector('tbody').rows);
expect(outerTable).toBeTruthy();
expect(outerRows.map(row => row.cells.length)).toEqual([3, 3, 3, 3]);
expect(innerTable).toBeTruthy();
expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3, 3]);
});
it('should be able to show a message when no data is being displayed in a native table', () => {
const fixture = TestBed.createComponent(NativeHtmlTableApp);
fixture.detectChanges();
// Assert that the data is inside the tbody specifically.
const tbody = fixture.nativeElement.querySelector('tbody')!;
const dataSource = fixture.componentInstance.dataSource!;
const initialData = dataSource.data;
expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy();
dataSource.data = [];
fixture.detectChanges();
const noDataRow: HTMLElement = tbody.querySelector('.mat-mdc-no-data-row');
expect(noDataRow).toBeTruthy();
expect(noDataRow.getAttribute('role')).toBe('row');
dataSource.data = initialData;
fixture.detectChanges();
expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy();
});
it('should be able to show a message when no data is being displayed', () => {
const fixture = TestBed.createComponent(MatTableApp);
fixture.detectChanges();
// Assert that the data is inside the tbody specifically.
const tbody = fixture.nativeElement.querySelector('tbody')!;
const initialData = fixture.componentInstance.dataSource!.data;
expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy();
fixture.componentInstance.dataSource!.data = [];
fixture.detectChanges();
const noDataRow: HTMLElement = tbody.querySelector('.mat-mdc-no-data-row');
expect(noDataRow).toBeTruthy();
expect(noDataRow.getAttribute('role')).toBe('row');
fixture.componentInstance.dataSource!.data = initialData;
fixture.detectChanges();
expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy();
});
it('should show the no data row if there is no data on init', () => {
const fixture = TestBed.createComponent(MatTableApp);
fixture.componentInstance.dataSource!.data = [];
fixture.detectChanges();
const tbody = fixture.nativeElement.querySelector('tbody')!;
expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeTruthy();
});
it('should set the content styling class on the tbody', () => {
let fixture = TestBed.createComponent(NativeHtmlTableApp);
fixture.detectChanges();
const tbodyElement = fixture.nativeElement.querySelector('tbody');
expect(tbodyElement.classList).toContain('mdc-data-table__content');
});
});
it('should render with MatTableDataSource and sort', () => {
let fixture = TestBed.createComponent(MatTableWithSortApp);
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table')!;
const data = fixture.componentInstance.dataSource!.data;
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[data[0].a, data[0].b, data[0].c],
[data[1].a, data[1].b, data[1].c],
[data[2].a, data[2].b, data[2].c],
]);
});
it('should render with MatTableDataSource and pagination', () => {
let fixture = TestBed.createComponent(MatTableWithPaginatorApp);
fixture.detectChanges();
const tableElement = fixture.nativeElement.querySelector('table')!;
const data = fixture.componentInstance.dataSource!.data;
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[data[0].a, data[0].b, data[0].c],
[data[1].a, data[1].b, data[1].c],
[data[2].a, data[2].b, data[2].c],
]);
});
it('should apply custom sticky CSS class to sticky cells', fakeAsync(() => {
let fixture = TestBed.createComponent(StickyTableApp);
fixture.detectChanges();
flushMicrotasks();
const stuckCellElement = fixture.nativeElement.querySelector('table th')!;
expect(stuckCellElement.classList).toContain('mat-mdc-table-sticky');
}));
// Note: needs to be fakeAsync so it catches the error.
it('should not throw when a row definition is on an ng-container', fakeAsync(() => {
const fixture = TestBed.createComponent(TableWithNgContainerRow);
expect(() => {
fixture.detectChanges();
tick();
}).not.toThrow();
}));
it('should be able to render a flexbox-based table', () => {
expect(() => {
const fixture = TestBed.createComponent(MatFlexTableApp);
fixture.detectChanges();
}).not.toThrow();
});
describe('with MatTableDataSource and sort/pagination/filter', () => {
let tableElement: HTMLElement;
let fixture: ComponentFixture<ArrayDataSourceMatTableApp>;
let dataSource: MatTableDataSource<TestData>;
let component: ArrayDataSourceMatTableApp;
beforeEach(() => {
fixture = TestBed.createComponent(ArrayDataSourceMatTableApp);
fixture.detectChanges();
tableElement = fixture.nativeElement.querySelector('table');
component = fixture.componentInstance;
dataSource = fixture.componentInstance.dataSource;
});
it('should create table and display data source contents', () => {
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('changing data should update the table contents', () => {
// Add data
component.underlyingDataSource.addData();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Remove data
const modifiedData = dataSource.data.slice();
modifiedData.shift();
dataSource.data = modifiedData;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should update the page index when switching to a smaller data set from a page', fakeAsync(() => {
// Add 20 rows so we can switch pages.
for (let i = 0; i < 20; i++) {
component.underlyingDataSource.addData();
fixture.detectChanges();
tick();
fixture.detectChanges();
}
// Go to the last page.
fixture.componentInstance.paginator.lastPage();
fixture.detectChanges();
// Switch to a smaller data set.
dataSource.data = [{a: 'a_0', b: 'b_0', c: 'c_0'}];
fixture.detectChanges();
tick();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_0', 'b_0', 'c_0'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
it('should be able to filter the table contents', fakeAsync(() => {
// Change filter to a_1, should match one row
dataSource.filter = 'a_1';
flushMicrotasks(); // Resolve promise that updates paginator's length
fixture.detectChanges();
expect(dataSource.filteredData.length).toBe(1);
expect(dataSource.filteredData[0]).toBe(dataSource.data[0]);
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
expect(dataSource.paginator!.length).toBe(1);
// Change filter to ' A_2 ', should match one row (ignores case and whitespace)
dataSource.filter = ' A_2 ';
flushMicrotasks();
fixture.detectChanges();
expect(dataSource.filteredData.length).toBe(1);
expect(dataSource.filteredData[0]).toBe(dataSource.data[1]);
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change filter to empty string, should match all rows
dataSource.filter = '';
flushMicrotasks();
fixture.detectChanges();
expect(dataSource.filteredData.length).toBe(3);
expect(dataSource.filteredData[0]).toBe(dataSource.data[0]);
expect(dataSource.filteredData[1]).toBe(dataSource.data[1]);
expect(dataSource.filteredData[2]).toBe(dataSource.data[2]);
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change filter function and filter, should match to rows with zebra.
dataSource.filterPredicate = (data, filter) => {
let dataStr;
switch (data.a) {
case 'a_1':
dataStr = 'elephant';
break;
case 'a_2':
dataStr = 'zebra';
break;
case 'a_3':
dataStr = 'monkey';
break;
default:
dataStr = '';
}
return dataStr.indexOf(filter) != -1;
};
dataSource.filter = 'zebra';
flushMicrotasks();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change the filter to a falsy value that might come in from the view.
dataSource.filter = 0 as any;
flushMicrotasks();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
it('should not match concatenated words', fakeAsync(() => {
// Set the value to the last character of the first
// column plus the first character of the second column.
dataSource.filter = '1b';
flushMicrotasks();
fixture.detectChanges();
expect(dataSource.filteredData.length).toBe(0);
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
it('should be able to sort the table contents', () => {
// Activate column A sort
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Activate column A sort again (reverse direction)
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['a_1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change sort function to customize how it sorts - first column 1, then 3, then 2
dataSource.sortingDataAccessor = data => {
switch (data.a) {
case 'a_1':
return 'elephant';
case 'a_2':
return 'zebra';
case 'a_3':
return 'monkey';
default:
return '';
}
};
component.sort.direction = '';
component.sort.sort(component.sortHeader);
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should by default correctly sort an empty string', () => {
// Activate column A sort
dataSource.data[0].a = ' ';
component.sort.sort(component.sortHeader);
fixture.detectChanges();
// Expect that empty string row comes before the other values
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Expect that empty string row comes before the other values
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should by default correctly sort undefined values', () => {
// Activate column A sort
dataSource.data[0].a = undefined;
// Expect that undefined row comes before the other values
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Expect that undefined row comes after the other values
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should sort zero correctly', () => {
// Activate column A sort
dataSource.data[0].a = 1;
dataSource.data[1].a = 0;
dataSource.data[2].a = -1;
// Expect that zero comes after the negative numbers and before the positive ones.
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['-1', 'b_3', 'c_3'],
['0', 'b_2', 'c_2'],
['1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Expect that zero comes after the negative numbers and before
// the positive ones when switching the sorting direction.
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['1', 'b_1', 'c_1'],
['0', 'b_2', 'c_2'],
['-1', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should be able to page the table contents', fakeAsync(() => {
// Add 100 rows, should only display first 5 since page length is 5
for (let i = 0; i < 100; i++) {
component.underlyingDataSource.addData();
}
fixture.detectChanges();
flushMicrotasks(); // Resolve promise that updates paginator's length
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['a_5', 'b_5', 'c_5'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Navigate to the next page
component.paginator.nextPage();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_6', 'b_6', 'c_6'],
['a_7', 'b_7', 'c_7'],
['a_8', 'b_8', 'c_8'],
['a_9', 'b_9', 'c_9'],
['a_10', 'b_10', 'c_10'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
it('should sort strings with numbers larger than MAX_SAFE_INTEGER correctly', () => {
const large = '9563256840123535';
const larger = '9563256840123536';
const largest = '9563256840123537';
dataSource.data[0].a = largest;
dataSource.data[1].a = larger;
dataSource.data[2].a = large;
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[large, 'b_3', 'c_3'],
[larger, 'b_2', 'c_2'],
[largest, 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
component.sort.sort(component.sortHeader);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
[largest, 'b_1', 'c_1'],
[larger, 'b_2', 'c_2'],
[large, 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
it('should fall back to empty table if invalid data is passed in', () => {
component.underlyingDataSource.addData();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['Footer A', 'Footer B', 'Footer C'],
]);
dataSource.data = null!;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Footer A', 'Footer B', 'Footer C'],
]);
component.underlyingDataSource.addData();
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['a_5', 'b_5', 'c_5'],
['Footer A', 'Footer B', 'Footer C'],
]);
dataSource.data = {} as any;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
});
});
interface TestData {
a: string | number | undefined;
b: string | number | undefined;
c: string | number | undefined;
}
class FakeDataSource extends DataSource<TestData> {
_dataChange = new BehaviorSubject<TestData[]>([]);
get data() {
return this._dataChange.getValue();
}
set data(data: TestData[]) {
this._dataChange.next(data);
}
constructor() {
super();
for (let i = 0; i < 4; i++) {
this.addData();
}
}
connect(): Observable<TestData[]> {
return this._dataChange;
}
disconnect() {}
addData() {
const nextIndex = this.data.length + 1;
let copiedData = this.data.slice();
copiedData.push({
a: `a_${nextIndex}`,
b: `b_${nextIndex}`,
c: `c_${nextIndex}`,
});
this.data = copiedData;
}
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
<td mat-footer-cell *matFooterCellDef> Footer A</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
<td mat-footer-cell *matFooterCellDef> Footer B</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
<td mat-footer-cell *matFooterCellDef> Footer C</td>
</ng-container>
<ng-container matColumnDef="special_column">
<td mat-cell *matCellDef="let row"> fourth_row </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></tr>
<tr *matNoDataRow>
<td>No data</td>
</tr>
<tr mat-footer-row *matFooterRowDef="columnsToRender"></tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class MatTableApp {
dataSource: FakeDataSource | null = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
isFourthRow = (i: number, _rowData: TestData) => i == 3;
@ViewChild(MatTable) table: MatTable<TestData>;
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
<tr *matNoDataRow>
<td>No data</td>
</tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class NativeHtmlTableApp {
dataSource: FakeDataSource | null = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(MatTable) table: MatTable<TestData>;
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef>Column A</th>
<td mat-cell *matCellDef="let row">{{row.a}}</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef>Column B</th>
<td mat-cell *matCellDef="let row">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
<td mat-footer-cell *matFooterCellDef> Footer A</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
<td mat-footer-cell *matFooterCellDef> Footer B</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
<td mat-footer-cell *matFooterCellDef> Footer C</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
</table>
</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef>Column C</th>
<td mat-cell *matCellDef="let row">{{row.c}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class NestedTableApp {
dataSource: FakeDataSource | null = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A </th>
<td mat-cell *matCellDef="let row"> {{row.a}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class StickyTableApp {
dataSource = new FakeDataSource();
columnsToRender = ['column_a'];
@ViewChild(MatTable) table: MatTable<TestData>;
}
@Component({
template: `
<table mat-table [dataSource]="dataSource" [multiTemplateDataRows]="multiTemplateDataRows">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
<td mat-footer-cell *matFooterCellDef> Footer A</td>
</ng-container>
<ng-container matColumnDef="special_column">
<td mat-cell *matCellDef="let row"> fourth_row </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['column_a']"></tr>
<tr mat-row *matRowDef="let row; columns: ['column_a']"></tr>
<tr mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></tr>
<tr mat-footer-row *matFooterRowDef="['column_a']"></tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class MatTableWithWhenRowApp {
multiTemplateDataRows = false;
dataSource: FakeDataSource | null = new FakeDataSource();
isFourthRow = (i: number, _rowData: TestData) => i == 3;
@ViewChild(MatTable) table: MatTable<TestData>;
}
@Component({
template: `
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef mat-sort-header="a"> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
<td mat-footer-cell *matFooterCellDef> Footer A</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
<td mat-footer-cell *matFooterCellDef> Footer B</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
<td mat-footer-cell *matFooterCellDef> Footer C</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
<tr mat-footer-row *matFooterRowDef="columnsToRender"></tr>
</table>
<mat-paginator [pageSize]="5"></mat-paginator>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class ArrayDataSourceMatTableApp implements AfterViewInit {
underlyingDataSource = new FakeDataSource();
dataSource = new MatTableDataSource<TestData>();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(MatTable) table: MatTable<TestData>;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ViewChild(MatSortHeader) sortHeader: MatSortHeader;
constructor() {
this.underlyingDataSource.data = [];
// Add three rows of data
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.connect().subscribe(data => {
this.dataSource.data = data;
});
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
}
@Component({
template: `
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef mat-sort-header="a"> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class MatTableWithSortApp implements OnInit {
underlyingDataSource = new FakeDataSource();
dataSource = new MatTableDataSource<TestData>();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(MatTable) table: MatTable<TestData>;
@ViewChild(MatSort) sort: MatSort;
constructor() {
this.underlyingDataSource.data = [];
// Add three rows of data
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.connect().subscribe(data => {
this.dataSource.data = data;
});
}
ngOnInit() {
this.dataSource!.sort = this.sort;
}
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef> Column A</th>
<td mat-cell *matCellDef="let row"> {{row.a}}</td>
</ng-container>
<ng-container matColumnDef="column_b">
<th mat-header-cell *matHeaderCellDef> Column B</th>
<td mat-cell *matCellDef="let row"> {{row.b}}</td>
</ng-container>
<ng-container matColumnDef="column_c">
<th mat-header-cell *matHeaderCellDef> Column C</th>
<td mat-cell *matCellDef="let row"> {{row.c}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
</table>
<mat-paginator [pageSize]="5"></mat-paginator>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class MatTableWithPaginatorApp implements OnInit {
underlyingDataSource = new FakeDataSource();
dataSource = new MatTableDataSource<TestData>();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(MatTable) table: MatTable<TestData>;
@ViewChild(MatPaginator) paginator: MatPaginator;
constructor() {
this.underlyingDataSource.data = [];
// Add three rows of data
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.addData();
this.underlyingDataSource.connect().subscribe(data => {
this.dataSource.data = data;
});
}
ngOnInit() {
this.dataSource!.paginator = this.paginator;
}
}
@Component({
template: `
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<th mat-header-cell *matHeaderCellDef>Column A</th>
<td mat-cell *matCellDef="let row">{{row.a}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<ng-container *matRowDef="let row; columns: columnsToRender">
<tr mat-row></tr>
</ng-container>
</table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class TableWithNgContainerRow {
dataSource: FakeDataSource | null = new FakeDataSource();
columnsToRender = ['column_a'];
}
@Component({
template: `
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="column_a">
<mat-header-cell *matHeaderCellDef> Column A</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.a}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer A</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_b">
<mat-header-cell *matHeaderCellDef> Column B</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.b}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer B</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_c">
<mat-header-cell *matHeaderCellDef> Column C</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.c}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer C</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="special_column">
<mat-cell *matCellDef="let row"> fourth_row </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnsToRender"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnsToRender"></mat-row>
<div *matNoDataRow>No data</div>
<mat-footer-row *matFooterRowDef="columnsToRender"></mat-footer-row>
</mat-table>
`,
standalone: true,
imports: [MatTableModule, MatPaginatorModule, MatSortModule],
})
class MatFlexTableApp {
dataSource: FakeDataSource | null = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(MatTable) table: MatTable<TestData>;
}
function getElements(element: Element, query: string): Element[] {
return [].slice.call(element.querySelectorAll(query));
}
function getHeaderRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.mat-mdc-header-row'))!;
}
function getFooterRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.mat-mdc-footer-row'))!;
}
function getRows(tableElement: Element): Element[] {
return getElements(tableElement, '.mat-mdc-row');
}
function getCells(row: Element): Element[] {
if (!row) {
return [];
}
return getElements(row, 'td');
}
function getHeaderCells(headerRow: Element): Element[] {
return getElements(headerRow, 'th');
}
function getFooterCells(footerRow: Element): Element[] {
return getElements(footerRow, 'td');
}
function getActualTableContent(tableElement: Element): string[][] {
let actualTableContent: Element[][] = [];
getHeaderRows(tableElement).forEach(row => {
actualTableContent.push(getHeaderCells(row));
});
// Check data row cells
const rows = getRows(tableElement).map(row => getCells(row));
actualTableContent = actualTableContent.concat(rows);
getFooterRows(tableElement).forEach(row => {
actualTableContent.push(getFooterCells(row));
});
// Convert the nodes into their text content;
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
}
export function expectTableToMatchContent(tableElement: Element, expected: any[]) {
const missedExpectations: string[] = [];
function checkCellContent(actualCell: string, expectedCell: string) {
if (actualCell !== expectedCell) {
missedExpectations.push(`Expected cell contents to be ${expectedCell} but was ${actualCell}`);
}
}
const actual = getActualTableContent(tableElement);
// Make sure the number of rows match
if (actual.length !== expected.length) {
missedExpectations.push(`Expected ${expected.length} total rows but got ${actual.length}`);
fail(missedExpectations.join('\n'));
}
actual.forEach((row, rowIndex) => {
const expectedRow = expected[rowIndex];
// Make sure the number of cells match
if (row.length !== expectedRow.length) {
missedExpectations.push(`Expected ${expectedRow.length} cells in row but got ${row.length}`);
fail(missedExpectations.join('\n'));
}
row.forEach((actualCell, cellIndex) => {
const expectedCell = expectedRow ? expectedRow[cellIndex] : null;
checkCellContent(actualCell, expectedCell);
});
});
if (missedExpectations.length) {
fail(missedExpectations.join('\n'));
}
}