UNPKG

ipsos-components

Version:

Material Design components for Angular

430 lines (368 loc) 15.2 kB
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, ElementRef, EmbeddedViewRef, Input, isDevMode, IterableChangeRecord, IterableDiffer, IterableDiffers, NgIterable, QueryList, TrackByFunction, ViewChild, ViewContainerRef, ViewEncapsulation, } from '@angular/core'; import {CollectionViewer, DataSource} from '@angular/cdk/collections'; import {CdkCellOutlet, CdkCellOutletRowContext, CdkHeaderRowDef, CdkRowDef} from './row'; import {takeUntil} from 'rxjs/operators/takeUntil'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Subscription} from 'rxjs/Subscription'; import {Subject} from 'rxjs/Subject'; import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell'; import { getTableDuplicateColumnNameError, getTableMissingMatchingRowDefError, getTableMissingRowDefsError, getTableMultipleDefaultRowDefsError, getTableUnknownColumnError } from './table-errors'; /** * Provides a handle for the table to grab the view container's ng-container to insert data rows. * @docs-private */ @Directive({selector: '[rowPlaceholder]'}) export class RowPlaceholder { constructor(public viewContainer: ViewContainerRef) { } } /** * Provides a handle for the table to grab the view container's ng-container to insert the header. * @docs-private */ @Directive({selector: '[headerRowPlaceholder]'}) export class HeaderRowPlaceholder { constructor(public viewContainer: ViewContainerRef) { } } /** * The table template that can be used by the mat-table. Should not be used outside of the * material library. */ export const CDK_TABLE_TEMPLATE = ` <ng-container headerRowPlaceholder></ng-container> <ng-container rowPlaceholder></ng-container>`; /** * Class used to conveniently type the embedded view ref for rows with a context. * @docs-private */ abstract class RowViewRef<T> extends EmbeddedViewRef<CdkCellOutletRowContext<T>> { } /** * A data table that connects with a data source to retrieve data of type `T` and renders * a header row and data rows. Updates the rows when new data is provided by the data source. */ @Component({ moduleId: module.id, selector: 'cdk-table', exportAs: 'cdkTable', template: CDK_TABLE_TEMPLATE, host: { 'class': 'cdk-table', }, encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, changeDetection: ChangeDetectionStrategy.OnPush, }) export class CdkTable<T> implements CollectionViewer { /** Subject that emits when the component has been destroyed. */ private _onDestroy = new Subject<void>(); /** Latest data provided by the data source through the connect interface. */ private _data: NgIterable<T> = []; /** Subscription that listens for the data provided by the data source. */ private _renderChangeSubscription: Subscription | null; /** Map of all the user's defined columns (header and data cell template) identified by name. */ private _columnDefsByName = new Map<string, CdkColumnDef>(); /** Differ used to find the changes in the data provided by the data source. */ private _dataDiffer: IterableDiffer<T>; /** Stores the row definition that does not have a when predicate. */ private _defaultRowDef: CdkRowDef<T> | null; /** * Tracking function that will be used to check the differences in data changes. Used similarly * to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data * relative to the function to know if a row should be added/removed/moved. * Accepts a function that takes two parameters, `index` and `item`. */ @Input() get trackBy(): TrackByFunction<T> { return this._trackByFn; } set trackBy(fn: TrackByFunction<T>) { if (isDevMode() && fn != null && typeof fn !== 'function' && <any>console && <any>console.warn) { console.warn(`trackBy must be a function, but received ${JSON.stringify(fn)}.`); } this._trackByFn = fn; } private _trackByFn: TrackByFunction<T>; /** * Provides a stream containing the latest data array to render. Influenced by the table's * stream of view window (what rows are currently on screen). */ @Input() get dataSource(): DataSource<T> { return this._dataSource; } set dataSource(dataSource: DataSource<T>) { if (this._dataSource !== dataSource) { this._switchDataSource(dataSource); } } private _dataSource: DataSource<T>; // TODO(andrewseguin): Remove max value as the end index // and instead calculate the view on init and scroll. /** * Stream containing the latest information on what rows are being displayed on screen. * Can be used by the data source to as a heuristic of what data should be provided. */ viewChange = new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE}); // Placeholders within the table's template where the header and data rows will be inserted. @ViewChild(RowPlaceholder) _rowPlaceholder: RowPlaceholder; @ViewChild(HeaderRowPlaceholder) _headerRowPlaceholder: HeaderRowPlaceholder; /** * The column definitions provided by the user that contain what the header and cells should * render for each column. */ @ContentChildren(CdkColumnDef) _columnDefs: QueryList<CdkColumnDef>; /** Template definition used as the header container. */ @ContentChild(CdkHeaderRowDef) _headerDef: CdkHeaderRowDef; /** Set of template definitions that used as the data row containers. */ @ContentChildren(CdkRowDef) _rowDefs: QueryList<CdkRowDef<T>>; constructor(private readonly _differs: IterableDiffers, private readonly _changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, @Attribute('role') role: string) { if (!role) { elementRef.nativeElement.setAttribute('role', 'grid'); } } ngOnInit() { // TODO(andrewseguin): Setup a listener for scrolling, emit the calculated view to viewChange this._dataDiffer = this._differs.find([]).create(this._trackByFn); } ngAfterContentInit() { if (!this._headerDef && !this._rowDefs.length) { throw getTableMissingRowDefsError(); } this._cacheColumnDefsByName(); this._columnDefs.changes.subscribe(() => this._cacheColumnDefsByName()); this._renderHeaderRow(); } ngAfterContentChecked() { this._renderUpdatedColumns(); const defaultRowDefs = this._rowDefs.filter(def => !def.when); if (defaultRowDefs.length > 1) { throw getTableMultipleDefaultRowDefsError(); } this._defaultRowDef = defaultRowDefs[0]; if (this.dataSource && !this._renderChangeSubscription) { this._observeRenderChanges(); } } ngOnDestroy() { this._rowPlaceholder.viewContainer.clear(); this._headerRowPlaceholder.viewContainer.clear(); this._onDestroy.next(); this._onDestroy.complete(); if (this.dataSource) { this.dataSource.disconnect(this); } } /** Update the map containing the content's column definitions. */ private _cacheColumnDefsByName() { this._columnDefsByName.clear(); this._columnDefs.forEach(columnDef => { if (this._columnDefsByName.has(columnDef.name)) { throw getTableDuplicateColumnNameError(columnDef.name); } this._columnDefsByName.set(columnDef.name, columnDef); }); } /** * Check if the header or rows have changed what columns they want to display. If there is a diff, * then re-render that section. */ private _renderUpdatedColumns() { // Re-render the rows when the row definition columns change. this._rowDefs.forEach(def => { if (!!def.getColumnsDiff()) { // Reset the data to an empty array so that renderRowChanges will re-render all new rows. this._dataDiffer.diff([]); this._rowPlaceholder.viewContainer.clear(); this._renderRowChanges(); } }); // Re-render the header row if there is a difference in its columns. if (this._headerDef.getColumnsDiff()) { this._headerRowPlaceholder.viewContainer.clear(); this._renderHeaderRow(); } } /** * Switch to the provided data source by resetting the data and unsubscribing from the current * render change subscription if one exists. If the data source is null, interpret this by * clearing the row placeholder. Otherwise start listening for new data. */ private _switchDataSource(dataSource: DataSource<T>) { this._data = []; if (this.dataSource) { this.dataSource.disconnect(this); } // Stop listening for data from the previous data source. if (this._renderChangeSubscription) { this._renderChangeSubscription.unsubscribe(); this._renderChangeSubscription = null; } // Remove the table's rows if there is now no data source if (!dataSource) { this._rowPlaceholder.viewContainer.clear(); } this._dataSource = dataSource; } /** Set up a subscription for the data provided by the data source. */ private _observeRenderChanges() { this._renderChangeSubscription = this.dataSource.connect(this).pipe(takeUntil(this._onDestroy)) .subscribe(data => { this._data = data; this._renderRowChanges(); }); } /** * Create the embedded view for the header template and place it in the header row view container. */ private _renderHeaderRow() { const cells = this._getHeaderCellTemplatesForRow(this._headerDef); if (!cells.length) { return; } // TODO(andrewseguin): add some code to enforce that exactly // one CdkCellOutlet was instantiated as a result // of `createEmbeddedView`. this._headerRowPlaceholder.viewContainer .createEmbeddedView(this._headerDef.template, {cells}); cells.forEach(cell => { if (CdkCellOutlet.mostRecentCellOutlet) { CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, {}); } }); this._changeDetectorRef.markForCheck(); } /** * Check for changes made in the data and render each change (row added/removed/moved) and update * row contexts. */ private _renderRowChanges() { const changes = this._dataDiffer.diff(this._data); if (!changes) { return; } const viewContainer = this._rowPlaceholder.viewContainer; changes.forEachOperation( (record: IterableChangeRecord<T>, adjustedPreviousIndex: number, currentIndex: number) => { if (record.previousIndex == null) { this._insertRow(record.item, currentIndex); } else if (currentIndex == null) { viewContainer.remove(adjustedPreviousIndex); } else { const view = <RowViewRef<T>>viewContainer.get(adjustedPreviousIndex); viewContainer.move(view!, currentIndex); } }); // Update the meta context of a row's context data (index, count, first, last, ...) this._updateRowIndexContext(); // Update rows that did not get added/removed/moved but may have had their identity changed, // e.g. if trackBy matched data on some property but the actual data reference changed. changes.forEachIdentityChange((record: IterableChangeRecord<T>) => { const rowView = <RowViewRef<T>>viewContainer.get(record.currentIndex!); rowView.context.$implicit = record.item; }); } /** * Finds the matching row definition that should be used for this row data. If there is only * one row definition, it is returned. Otherwise, find the row definition that has a when * predicate that returns true with the data. If none return true, return the default row * definition. */ _getRowDef(data: T, i: number): CdkRowDef<T> { if (this._rowDefs.length == 1) { return this._rowDefs.first; } let rowDef = this._rowDefs.find(def => def.when && def.when(i, data)) || this._defaultRowDef; if (!rowDef) { throw getTableMissingMatchingRowDefError(); } return rowDef; } /** * Create the embedded view for the data row template and place it in the correct index location * within the data row view container. */ private _insertRow(rowData: T, index: number) { const row = this._getRowDef(rowData, index); // Row context that will be provided to both the created embedded row view and its cells. const context: CdkCellOutletRowContext<T> = {$implicit: rowData}; // TODO(andrewseguin): add some code to enforce that exactly one // CdkCellOutlet was instantiated as a result of `createEmbeddedView`. this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context, index); this._getCellTemplatesForRow(row).forEach(cell => { if (CdkCellOutlet.mostRecentCellOutlet) { CdkCellOutlet.mostRecentCellOutlet._viewContainer .createEmbeddedView(cell.template, context); } }); this._changeDetectorRef.markForCheck(); } /** * Updates the index-related context for each row to reflect any changes in the index of the rows, * e.g. first/last/even/odd. */ private _updateRowIndexContext() { const viewContainer = this._rowPlaceholder.viewContainer; for (let index = 0, count = viewContainer.length; index < count; index++) { const viewRef = viewContainer.get(index) as RowViewRef<T>; viewRef.context.index = index; viewRef.context.count = count; viewRef.context.first = index === 0; viewRef.context.last = index === count - 1; viewRef.context.even = index % 2 === 0; viewRef.context.odd = !viewRef.context.even; } } /** * Returns the cell template definitions to insert into the header * as defined by its list of columns to display. */ private _getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] { if (!headerDef.columns) { return []; } return headerDef.columns.map(columnId => { const column = this._columnDefsByName.get(columnId); if (!column) { throw getTableUnknownColumnError(columnId); } return column.headerCell; }); } /** * Returns the cell template definitions to insert in the provided row * as defined by its list of columns to display. */ private _getCellTemplatesForRow(rowDef: CdkRowDef<T>): CdkCellDef[] { if (!rowDef.columns) { return []; } return rowDef.columns.map(columnId => { const column = this._columnDefsByName.get(columnId); if (!column) { throw getTableUnknownColumnError(columnId); } return column.cell; }); } }