diff --git a/src/demo-app/table/custom-table/custom-table.ts b/src/demo-app/table/custom-table/custom-table.ts index 333647fb2b4..b61d79f4d9b 100644 --- a/src/demo-app/table/custom-table/custom-table.ts +++ b/src/demo-app/table/custom-table/custom-table.ts @@ -25,7 +25,7 @@ export class CustomTableDemo { @ViewChild('simpleTableSort') simpleTableSort: MatSort; @ViewChild('wrapperTableSort') wrapperTableSort: MatSort; - ngAfterViewInit() { + ngOnInit() { this.simpleTableDataSource.sort = this.simpleTableSort; this.wrapperTableDataSource.sort = this.wrapperTableSort; } diff --git a/src/demo-app/table/table-demo.ts b/src/demo-app/table/table-demo.ts index 6d30e0535a1..b024c04c14a 100644 --- a/src/demo-app/table/table-demo.ts +++ b/src/demo-app/table/table-demo.ts @@ -81,14 +81,10 @@ export class TableDemo { (data: UserData, filter: string) => data.name.indexOf(filter) != -1; } - ngAfterViewInit() { - // Needs to be set up after the view is initialized since the data source will look at the sort - // and paginator's initial values to know what data should be rendered. - this.matTableDataSource!.paginator = this.paginatorForDataSource; - this.matTableDataSource!.sort = this.sortForDataSource; - } - ngOnInit() { + this.matTableDataSource!.sort = this.sortForDataSource; + this.matTableDataSource!.paginator = this.paginatorForDataSource; + this.connect(); fromEvent(this.filter.nativeElement, 'keyup') .pipe( diff --git a/src/lib/paginator/paginator.spec.ts b/src/lib/paginator/paginator.spec.ts index 504615e3229..255afc78f90 100644 --- a/src/lib/paginator/paginator.spec.ts +++ b/src/lib/paginator/paginator.spec.ts @@ -1,4 +1,4 @@ -import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {async, ComponentFixture, TestBed, inject, tick, fakeAsync} from '@angular/core/testing'; import {MatPaginatorModule} from './index'; import {MatPaginator, PageEvent} from './paginator'; import {Component, ViewChild} from '@angular/core'; @@ -124,27 +124,33 @@ describe('MatPaginator', () => { expect(paginator.pageIndex).toBe(0); expect(component.latestPageEvent ? component.latestPageEvent.pageIndex : null).toBe(0); }); - }); it('should be able to show the first/last buttons', () => { expect(getFirstButton(fixture)) - .toBeNull('Expected first button to not exist.'); + .toBeNull('Expected first button to not exist.'); expect(getLastButton(fixture)) - .toBeNull('Expected last button to not exist.'); + .toBeNull('Expected last button to not exist.'); fixture.componentInstance.showFirstLastButtons = true; fixture.detectChanges(); expect(getFirstButton(fixture)) - .toBeTruthy('Expected first button to be rendered.'); + .toBeTruthy('Expected first button to be rendered.'); expect(getLastButton(fixture)) - .toBeTruthy('Expected last button to be rendered.'); - + .toBeTruthy('Expected last button to be rendered.'); }); + it('should mark itself as initialized', fakeAsync(() => { + let isMarkedInitialized = false; + paginator.initialized.subscribe(() => isMarkedInitialized = true); + + tick(); + expect(isMarkedInitialized).toBeTruthy(); + })); + describe('when showing the first and last button', () => { beforeEach(() => { diff --git a/src/lib/paginator/paginator.ts b/src/lib/paginator/paginator.ts index 005db034e21..7b0a59d99b9 100644 --- a/src/lib/paginator/paginator.ts +++ b/src/lib/paginator/paginator.ts @@ -20,6 +20,7 @@ import { } from '@angular/core'; import {Subscription} from 'rxjs'; import {MatPaginatorIntl} from './paginator-intl'; +import {HasInitialized, mixinInitialized} from '@angular/material/core'; /** The default page size if there is no page size and there are no provided page size options. */ const DEFAULT_PAGE_SIZE = 50; @@ -39,6 +40,11 @@ export class PageEvent { length: number; } +// Boilerplate for applying mixins to MatPaginator. +/** @docs-private */ +export class MatPaginatorBase {} +export const _MatPaginatorBase = mixinInitialized(MatPaginatorBase); + /** * Component to provide navigation between paged information. Displays the size of the current * page, user-selectable options to change that size, what items are being shown, and @@ -56,7 +62,7 @@ export class PageEvent { changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) -export class MatPaginator implements OnInit, OnDestroy { +export class MatPaginator extends _MatPaginatorBase implements OnInit, OnDestroy, HasInitialized { private _initialized: boolean; private _intlChanges: Subscription; @@ -121,12 +127,14 @@ export class MatPaginator implements OnInit, OnDestroy { constructor(public _intl: MatPaginatorIntl, private _changeDetectorRef: ChangeDetectorRef) { + super(); this._intlChanges = _intl.changes.subscribe(() => this._changeDetectorRef.markForCheck()); } ngOnInit() { this._initialized = true; this._updateDisplayedPageSizeOptions(); + this._markInitialized(); } ngOnDestroy() { diff --git a/src/lib/sort/sort.spec.ts b/src/lib/sort/sort.spec.ts index 4bbe69578c5..49004d79145 100644 --- a/src/lib/sort/sort.spec.ts +++ b/src/lib/sort/sort.spec.ts @@ -7,7 +7,7 @@ import { wrappedErrorMessage } from '@angular/cdk/testing'; import {Component, ElementRef, ViewChild} from '@angular/core'; -import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {Observable} from 'rxjs'; @@ -65,6 +65,14 @@ describe('MatSort', () => { expect(sortables.size).toBe(0); }); + it('should mark itself as initialized', fakeAsync(() => { + let isMarkedInitialized = false; + component.matSort.initialized.subscribe(() => isMarkedInitialized = true); + + tick(); + expect(isMarkedInitialized).toBeTruthy(); + })); + it('should use the column definition if used within a cdk table', () => { let cdkTableMatSortAppFixture = TestBed.createComponent(CdkTableMatSortApp); let cdkTableMatSortAppComponent = cdkTableMatSortAppFixture.componentInstance; diff --git a/src/lib/sort/sort.ts b/src/lib/sort/sort.ts index 9bffb9e6cb7..af0dc267c49 100644 --- a/src/lib/sort/sort.ts +++ b/src/lib/sort/sort.ts @@ -11,17 +11,18 @@ import { EventEmitter, Input, isDevMode, - Output, OnChanges, OnDestroy, + OnInit, + Output, } from '@angular/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {CanDisable, mixinDisabled} from '@angular/material/core'; +import {CanDisable, HasInitialized, mixinDisabled, mixinInitialized} from '@angular/material/core'; import {SortDirection} from './sort-direction'; import { - getSortInvalidDirectionError, getSortDuplicateSortableIdError, - getSortHeaderMissingIdError + getSortHeaderMissingIdError, + getSortInvalidDirectionError } from './sort-errors'; import {Subject} from 'rxjs'; @@ -49,7 +50,7 @@ export interface Sort { // Boilerplate for applying mixins to MatSort. /** @docs-private */ export class MatSortBase {} -export const _MatSortMixinBase = mixinDisabled(MatSortBase); +export const _MatSortMixinBase = mixinInitialized(mixinDisabled(MatSortBase)); /** Container for MatSortables to manage the sort state and provide default sort parameters. */ @Directive({ @@ -57,7 +58,8 @@ export const _MatSortMixinBase = mixinDisabled(MatSortBase); exportAs: 'matSort', inputs: ['disabled: matSortDisabled'] }) -export class MatSort extends _MatSortMixinBase implements CanDisable, OnChanges, OnDestroy { +export class MatSort extends _MatSortMixinBase + implements CanDisable, HasInitialized, OnChanges, OnDestroy, OnInit { /** Collection of all registered sortables that this directive manages. */ sortables = new Map(); @@ -145,6 +147,10 @@ export class MatSort extends _MatSortMixinBase implements CanDisable, OnChanges, return sortDirectionCycle[nextDirectionIndex]; } + ngOnInit() { + this._markInitialized(); + } + ngOnChanges() { this._stateChanges.next(); } diff --git a/src/lib/table/table-data-source.ts b/src/lib/table/table-data-source.ts index 00ce8fb5af2..d8e62ae9239 100644 --- a/src/lib/table/table-data-source.ts +++ b/src/lib/table/table-data-source.ts @@ -8,10 +8,17 @@ import {_isNumberValue} from '@angular/cdk/coercion'; import {DataSource} from '@angular/cdk/table'; +import { + BehaviorSubject, + combineLatest, + merge, + Observable, + of as observableOf, + Subscription +} from 'rxjs'; import {MatPaginator, PageEvent} from '@angular/material/paginator'; import {MatSort, Sort} from '@angular/material/sort'; -import {BehaviorSubject, combineLatest, EMPTY, Observable, Subscription} from 'rxjs'; -import {map, startWith} from 'rxjs/operators'; +import {map} from 'rxjs/operators'; /** @@ -174,9 +181,17 @@ export class MatTableDataSource extends DataSource { */ _updateChangeSubscription() { // Sorting and/or pagination should be watched if MatSort and/or MatPaginator are provided. - // Otherwise, use an empty observable stream to take their place. - const sortChange: Observable = this._sort ? this._sort.sortChange : EMPTY; - const pageChange: Observable = this._paginator ? this._paginator.page : EMPTY; + // The events should emit whenever the component emits a change or initializes, or if no + // component is provided, a stream with just a null event should be provided. + // The `sortChange` and `pageChange` acts as a signal to the combineLatests below so that the + // pipeline can progress to the next step. Note that the value from these streams are not used, + // they purely act as a signal to progress in the pipeline. + const sortChange: Observable = this._sort ? + merge(this._sort.sortChange, this._sort.initialized) : + observableOf(null); + const pageChange: Observable = this._paginator ? + merge(this._paginator.page, this._paginator.initialized) : + observableOf(null); if (this._renderChangesSubscription) { this._renderChangesSubscription.unsubscribe(); @@ -187,10 +202,10 @@ export class MatTableDataSource extends DataSource { const filteredData = combineLatest(dataStream, this._filter) .pipe(map(([data]) => this._filterData(data))); // Watch for filtered data or sort changes to provide an ordered set of data. - const orderedData = combineLatest(filteredData, sortChange.pipe(startWith(null!))) + const orderedData = combineLatest(filteredData, sortChange) .pipe(map(([data]) => this._orderData(data))); // Watch for ordered data or page changes to provide a paged set of data. - const paginatedData = combineLatest(orderedData, pageChange.pipe(startWith(null!))) + const paginatedData = combineLatest(orderedData, pageChange) .pipe(map(([data]) => this._pageData(data))); // Watched for paged data changes and send the result to the table to render. paginatedData.subscribe(data => this._renderData.next(data)); diff --git a/src/lib/table/table.spec.ts b/src/lib/table/table.spec.ts index 67ba1fc4f43..0e5e1f809cb 100644 --- a/src/lib/table/table.spec.ts +++ b/src/lib/table/table.spec.ts @@ -18,7 +18,9 @@ describe('MatTable', () => { MatTableApp, MatTableWithWhenRowApp, ArrayDataSourceMatTableApp, - NativeHtmlTableApp + NativeHtmlTableApp, + MatTableWithSortApp, + MatTableWithPaginatorApp, ], }).compileComponents(); })); @@ -69,7 +71,35 @@ describe('MatTable', () => { ]); }); - describe('with MatTableDataSource', () => { + it('should render with MatTableDataSource and sort', () => { + let fixture = TestBed.createComponent(MatTableWithSortApp); + fixture.detectChanges(); + + const tableElement = fixture.nativeElement.querySelector('.mat-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('.mat-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], + ]); + }); + + describe('with MatTableDataSource and sort/pagination/filter', () => { let tableElement: HTMLElement; let fixture: ComponentFixture; let dataSource: MatTableDataSource; @@ -504,14 +534,113 @@ class ArrayDataSourceMatTableApp { }); } - ngAfterViewInit() { - // Needs to be set up after the view is initialized since the data source will look at the sort - // and paginator's initial values to know what data should be rendered. + ngOnInit() { this.dataSource!.sort = this.sort; this.dataSource!.paginator = this.paginator; } } + +@Component({ + template: ` + + + Column A + {{row.a}} + + + + Column B + {{row.b}} + + + + Column C + {{row.c}} + + + + + + ` +}) +class MatTableWithSortApp { + underlyingDataSource = new FakeDataSource(); + dataSource = new MatTableDataSource(); + columnsToRender = ['column_a', 'column_b', 'column_c']; + + @ViewChild(MatTable) table: MatTable; + @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: ` + + + Column A + {{row.a}} + + + + Column B + {{row.b}} + + + + Column C + {{row.c}} + + + + + + + + ` +}) +class MatTableWithPaginatorApp { + underlyingDataSource = new FakeDataSource(); + dataSource = new MatTableDataSource(); + columnsToRender = ['column_a', 'column_b', 'column_c']; + + @ViewChild(MatTable) table: MatTable; + @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; + } +} + // Utilities copied from CDKTable's spec function getElements(element: Element, query: string): Element[] { return [].slice.call(element.querySelectorAll(query)); diff --git a/src/material-examples/table-overview/table-overview-example.ts b/src/material-examples/table-overview/table-overview-example.ts index 88ef1ff5c67..4d807e2d0b0 100644 --- a/src/material-examples/table-overview/table-overview-example.ts +++ b/src/material-examples/table-overview/table-overview-example.ts @@ -25,11 +25,7 @@ export class TableOverviewExample { this.dataSource = new MatTableDataSource(users); } - /** - * Set the paginator and sort after the view init since this component will - * be able to query its view for the initialized paginator and sort. - */ - ngAfterViewInit() { + ngOnInit() { this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } diff --git a/src/material-examples/table-pagination/table-pagination-example.ts b/src/material-examples/table-pagination/table-pagination-example.ts index bca8dac8093..2d10a411f4a 100644 --- a/src/material-examples/table-pagination/table-pagination-example.ts +++ b/src/material-examples/table-pagination/table-pagination-example.ts @@ -15,11 +15,7 @@ export class TablePaginationExample { @ViewChild(MatPaginator) paginator: MatPaginator; - /** - * Set the paginator after the view init since this component will - * be able to query its view for the initialized paginator. - */ - ngAfterViewInit() { + ngOnInit() { this.dataSource.paginator = this.paginator; } } diff --git a/src/material-examples/table-sorting/table-sorting-example.ts b/src/material-examples/table-sorting/table-sorting-example.ts index f52722c9c99..d7c5a196c2f 100644 --- a/src/material-examples/table-sorting/table-sorting-example.ts +++ b/src/material-examples/table-sorting/table-sorting-example.ts @@ -15,11 +15,7 @@ export class TableSortingExample { @ViewChild(MatSort) sort: MatSort; - /** - * Set the sort after the view init since this component will - * be able to query its view for the initialized sort. - */ - ngAfterViewInit() { + ngOnInit() { this.dataSource.sort = this.sort; } }