feat(data-table): minimal beginning implementation (#4468)

* feat(cdk-table): initial version

* add files

* remove unnecessary deps

* add test

* add tests

* edit comments

* minor style changes

* changes due to comments

* add test for column classes

* add OnPush to rest of components

* remove double quotes from colors.ts

* lint fies

* add comments to demo css

* comments

* split directives into separate files

* lint

* add content check in tests

* renaming directives

* remove fdescribe

* rename private placeholders; adjust comments; change CollectionViewer

* lint

* change jasmine array param

* remove unnecessary import from spec

* move out of core; add console warning about instability

* move jasmine-matchers

* fix demo
This commit is contained in:
Andrew Seguin
2017-05-25 16:50:32 -07:00
committed by tinayuangao
parent 5c489497c3
commit 7db2762d78
21 changed files with 2089 additions and 22 deletions
@@ -0,0 +1,40 @@
<div class="demo-table-container mat-elevation-z4">
<cdk-table #table [dataSource]="dataSource">
<!-- Column Definition: ID -->
<ng-container cdkColumnDef="userId">
<cdk-header-cell *cdkHeaderCellDef> ID </cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.id}} </cdk-cell>
</ng-container>
<!-- Column Definition: Progress -->
<ng-container cdkColumnDef="progress">
<cdk-header-cell *cdkHeaderCellDef> Progress </cdk-header-cell>
<cdk-cell *cdkCellDef="let row">
<div class="demo-progress-stat">{{row.progress}}%</div>
<div class="demo-progress-indicator-container">
<div class="demo-progress-indicator"
[style.background]="row.progress > 50 ? 'green' : 'red'"
[style.opacity]="getOpacity(row.progress)"
[style.width.%]="row.progress"></div>
</div>
</cdk-cell>
</ng-container>
<!-- Column Definition: Name -->
<ng-container cdkColumnDef="userName">
<cdk-header-cell *cdkHeaderCellDef> Name </cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.name}} </cdk-cell>
</ng-container>
<!-- Column Definition: Color -->
<ng-container cdkColumnDef="color">
<cdk-header-cell *cdkHeaderCellDef>Color</cdk-header-cell>
<cdk-cell *cdkCellDef="let row" [style.color]="row.color"> {{row.color}} </cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="propertiesToDisplay"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: propertiesToDisplay"></cdk-row>
</cdk-table>
</div>
@@ -0,0 +1,89 @@
/* Structure */
.demo-table-container {
display: flex;
flex-direction: column;
max-height: 800px;
// Table fills in the remaining area with a scroll
.cdk-table {
flex: 1 1 auto;
overflow: auto;
}
}
/*
* Styles to make the demo's cdk-table match the material design spec
* https://material.io/guidelines/components/data-tables.html
*/
.cdk-table {
display: block;
background: white;
}
.cdk-row, .cdk-header-row {
display: flex;
border-bottom: 1px solid #ccc;
align-items: center;
height: 48px;
padding: 0 8px;
}
.cdk-cell, .cdk-header-cell {
width: 150px;
padding: 10px;
position: relative;
flex: 1;
}
.cdk-header-cell {
font-size: 12px;
font-weight: bold;
color: rgba(0, 0, 0, 0.54);
}
.cdk-cell {
font-size: 13px;
color: rgba(0, 0, 0, 0.87);
}
/* Column and cell styles */
.cdk-column-userId {
max-width: 32px;
}
.cdk-column-userName {
max-width: 80px;
}
.cdk-column-progress {
&.cdk-cell {
display: flex;
position: relative;
.demo-progress-stat {
width: 32px;
padding-right: 8px;
}
.demo-progress-indicator-container {
position: relative;
flex-grow: 1;
display: flex;
align-items: center;
}
.demo-progress-indicator {
border-radius: 8px;
height: 8px;
}
}
}
.cdk-column-color {
max-width: 160px;
&.cdk-cell {
font-weight: bold;
font-size: 16px;
font-family: cursive;
}
}
@@ -0,0 +1,25 @@
import {Component} from '@angular/core';
import {PeopleDatabase} from './people-database';
import {PersonDataSource} from './person-data-source';
@Component({
moduleId: module.id,
selector: 'data-table-demo',
templateUrl: 'data-table-demo.html',
styleUrls: ['data-table-demo.css'],
})
export class DataTableDemo {
dataSource: PersonDataSource;
propertiesToDisplay = ['userId', 'userName', 'progress', 'color'];
constructor(private _peopleDatabase: PeopleDatabase) { }
ngOnInit() {
this.dataSource = new PersonDataSource(this._peopleDatabase);
}
getOpacity(progress: number) {
let distanceFromMiddle = Math.abs(50 - progress);
return distanceFromMiddle / 50 + .3;
}
}
@@ -0,0 +1,34 @@
import {Injectable} from '@angular/core';
import {NAMES} from '../dataset/names';
import {COLORS} from '../dataset/colors';
export let LATEST_ID: number = 0;
export interface UserData {
id: string;
name: string;
progress: string;
color: string;
}
@Injectable()
export class PeopleDatabase {
data: UserData[] = [];
constructor() {
for (let i = 0; i < 100; i++) { this.addPerson(); }
}
addPerson() {
const name =
NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' +
NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.';
this.data.push({
id: (++LATEST_ID).toString(),
name: name,
progress: Math.round(Math.random() * 100).toString(),
color: COLORS[Math.round(Math.random() * (COLORS.length - 1))]
});
}
}
@@ -0,0 +1,29 @@
import {CollectionViewer, DataSource} from '@angular/material';
import {Observable} from 'rxjs/Observable';
import {PeopleDatabase, UserData} from './people-database';
export class PersonDataSource extends DataSource<any> {
_renderedData: any[] = [];
constructor(private _peopleDatabase: PeopleDatabase) {
super();
}
connect(collectionViewer: CollectionViewer): Observable<UserData[]> {
return collectionViewer.viewChanged.map((view: {start: number, end: number}) => {
// Set the rendered rows length to the virtual page size. Fill in the data provided
// from the index start until the end index or pagination size, whichever is smaller.
this._renderedData.length = this._peopleDatabase.data.length;
const buffer = 20;
let rangeStart = Math.max(0, view.start - buffer);
let rangeEnd = Math.min(this._peopleDatabase.data.length, view.end + buffer);
for (let i = rangeStart; i < rangeEnd; i++) {
this._renderedData[i] = this._peopleDatabase.data[i];
}
return this._renderedData;
});
}
}
+149
View File
@@ -0,0 +1,149 @@
export const COLORS = [
'AliceBlue',
'AntiqueWhite',
'Aqua',
'Aquamarine',
'Azure',
'Beige',
'Bisque',
'Black',
'BlanchedAlmond',
'Blue',
'BlueViolet',
'Brown',
'BurlyWood',
'CadetBlue',
'Chartreuse',
'Chocolate',
'Coral',
'CornflowerBlue',
'Cornsilk',
'Crimson',
'Cyan',
'DarkBlue',
'DarkCyan',
'DarkGoldenRod',
'DarkGray',
'DarkGrey',
'DarkGreen',
'DarkKhaki',
'DarkMagenta',
'DarkOliveGreen',
'Darkorange',
'DarkOrchid',
'DarkRed',
'DarkSalmon',
'DarkSeaGreen',
'DarkSlateBlue',
'DarkSlateGray',
'DarkSlateGrey',
'DarkTurquoise',
'DarkViolet',
'DeepPink',
'DeepSkyBlue',
'DimGray',
'DimGrey',
'DodgerBlue',
'FireBrick',
'FloralWhite',
'ForestGreen',
'Fuchsia',
'Gainsboro',
'GhostWhite',
'Gold',
'GoldenRod',
'Gray',
'Grey',
'Green',
'GreenYellow',
'HoneyDew',
'HotPink',
'IndianRed',
'Indigo',
'Ivory',
'Khaki',
'Lavender',
'LavenderBlush',
'LawnGreen',
'LemonChiffon',
'LightBlue',
'LightCoral',
'LightCyan',
'LightGoldenRodYellow',
'LightGray',
'LightGrey',
'LightGreen',
'LightPink',
'LightSalmon',
'LightSeaGreen',
'LightSkyBlue',
'LightSlateGray',
'LightSlateGrey',
'LightSteelBlue',
'LightYellow',
'Lime',
'LimeGreen',
'Linen',
'Magenta',
'Maroon',
'MediumAquaMarine',
'MediumBlue',
'MediumOrchid',
'MediumPurple',
'MediumSeaGreen',
'MediumSlateBlue',
'MediumSpringGreen',
'MediumTurquoise',
'MediumVioletRed',
'MidnightBlue',
'MintCream',
'MistyRose',
'Moccasin',
'NavajoWhite',
'Navy',
'OldLace',
'Olive',
'OliveDrab',
'Orange',
'OrangeRed',
'Orchid',
'PaleGoldenRod',
'PaleGreen',
'PaleTurquoise',
'PaleVioletRed',
'PapayaWhip',
'PeachPuff',
'Peru',
'Pink',
'Plum',
'PowderBlue',
'Purple',
'Red',
'RosyBrown',
'RoyalBlue',
'SaddleBrown',
'Salmon',
'SandyBrown',
'SeaGreen',
'SeaShell',
'Sienna',
'Silver',
'SkyBlue',
'SlateBlue',
'SlateGray',
'SlateGrey',
'Snow',
'SpringGreen',
'SteelBlue',
'Tan',
'Teal',
'Thistle',
'Tomato',
'Turquoise',
'Violet',
'Wheat',
'White',
'WhiteSmoke',
'Yellow',
'YellowGreen'
];
File diff suppressed because it is too large Load Diff
+36 -7
View File
@@ -37,14 +37,40 @@ import {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputDemo} from './input/input-demo';
import {StyleDemo} from './style/style-demo';
import {DataTableDemo} from './data-table/data-table-demo';
import {PeopleDatabase} from './data-table/people-database';
import {DatepickerDemo} from './datepicker/datepicker-demo';
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/material';
import {
MdAutocompleteModule, MdButtonModule, MdButtonToggleModule, MdCardModule, MdCheckboxModule,
MdChipsModule, MdCoreModule, MdDatepickerModule, MdDialogModule, MdGridListModule, MdIconModule,
MdInputModule, MdListModule, MdMenuModule, MdNativeDateModule, MdProgressBarModule, MdRadioModule,
MdProgressSpinnerModule, MdRippleModule, MdSelectModule, MdSidenavModule, MdSliderModule,
MdSlideToggleModule, MdSnackBarModule, MdTabsModule, MdToolbarModule, MdTooltipModule
CdkDataTableModule,
FullscreenOverlayContainer,
MdAutocompleteModule,
MdButtonModule,
MdButtonToggleModule,
MdCardModule,
MdCheckboxModule,
MdChipsModule,
MdCoreModule,
MdDatepickerModule,
MdDialogModule,
MdGridListModule,
MdIconModule,
MdInputModule,
MdListModule,
MdMenuModule,
MdNativeDateModule,
MdProgressBarModule,
MdProgressSpinnerModule,
MdRadioModule,
MdRippleModule,
MdSelectModule,
MdSidenavModule,
MdSliderModule,
MdSlideToggleModule,
MdSnackBarModule,
MdTabsModule,
MdToolbarModule,
MdTooltipModule,
OverlayContainer
} from '@angular/material';
/**
@@ -79,6 +105,7 @@ import {
MdToolbarModule,
MdTooltipModule,
MdNativeDateModule,
CdkDataTableModule,
]
})
export class DemoMaterialModule {}
@@ -102,6 +129,7 @@ export class DemoMaterialModule {}
CardDemo,
ChipsDemo,
CheckboxDemo,
DataTableDemo,
DatepickerDemo,
DemoApp,
DemoAppOnPush,
@@ -142,7 +170,8 @@ export class DemoMaterialModule {}
PlatformDemo,
],
providers: [
{provide: OverlayContainer, useClass: FullscreenOverlayContainer}
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},
PeopleDatabase
],
entryComponents: [
DemoApp,
+1
View File
@@ -40,6 +40,7 @@ export class DemoApp {
{name: 'Card', route: 'card'},
{name: 'Chips', route: 'chips'},
{name: 'Checkbox', route: 'checkbox'},
{name: 'Data Table', route: 'data-table'},
{name: 'Datepicker', route: 'datepicker'},
{name: 'Dialog', route: 'dialog'},
{name: 'Gestures', route: 'gestures'},
+2
View File
@@ -33,6 +33,7 @@ import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {InputDemo} from '../input/input-demo';
import {StyleDemo} from '../style/style-demo';
import {DatepickerDemo} from '../datepicker/datepicker-demo';
import {DataTableDemo} from '../data-table/data-table-demo';
export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
@@ -40,6 +41,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'button', component: ButtonDemo},
{path: 'card', component: CardDemo},
{path: 'chips', component: ChipsDemo},
{path: 'data-table', component: DataTableDemo},
{path: 'datepicker', component: DatepickerDemo},
{path: 'radio', component: RadioDemo},
{path: 'select', component: SelectDemo},
+63
View File
@@ -0,0 +1,63 @@
import {ContentChild, Directive, ElementRef, Input, Renderer2, TemplateRef} from '@angular/core';
/**
* Cell definition for a CDK data-table.
* Captures the template of a column's data row cell as well as cell-specific properties.
*/
@Directive({selector: '[cdkCellDef]'})
export class CdkCellDef {
constructor(public template: TemplateRef<any>) { }
}
/**
* Header cell definition for a CDK data-table.
* Captures the template of a column's header cell and as well as cell-specific properties.
*/
@Directive({selector: '[cdkHeaderCellDef]'})
export class CdkHeaderCellDef {
constructor(public template: TemplateRef<any>) { }
}
/**
* Column definition for the CDK data-table.
* Defines a set of cells available for a table column.
*/
@Directive({selector: '[cdkColumnDef]'})
export class CdkColumnDef {
@Input('cdkColumnDef') name: string;
@ContentChild(CdkCellDef) cell: CdkCellDef;
@ContentChild(CdkHeaderCellDef) headerCell: CdkHeaderCellDef;
}
/** Header cell template container that adds the right classes and role. */
@Directive({
selector: 'cdk-header-cell',
host: {
'class': 'cdk-header-cell',
'role': 'columnheader',
},
})
export class CdkHeaderCell {
constructor(private columnDef: CdkColumnDef,
private elementRef: ElementRef,
private renderer: Renderer2) {
this.renderer.addClass(elementRef.nativeElement, `cdk-column-${columnDef.name}`);
}
}
/** Cell template container that adds the right classes and role. */
@Directive({
selector: 'cdk-cell',
host: {
'class': 'cdk-cell',
'role': 'gridcell',
},
})
export class CdkCell {
constructor(private columnDef: CdkColumnDef,
private elementRef: ElementRef,
private renderer: Renderer2) {
this.renderer.addClass(elementRef.nativeElement, `cdk-column-${columnDef.name}`);
}
}
+9
View File
@@ -0,0 +1,9 @@
import {Observable} from 'rxjs/Observable';
export interface CollectionViewer {
viewChanged: Observable<{start: number, end: number}>;
}
export abstract class DataSource<T> {
abstract connect(collectionViewer: CollectionViewer): Observable<T[]>;
}
+255
View File
@@ -0,0 +1,255 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {CdkTable} from './data-table';
import {CollectionViewer, DataSource} from './data-source';
import {CommonModule} from '@angular/common';
import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {customMatchers} from '../testing/jasmine-matchers';
import {CdkDataTableModule} from './index';
describe('CdkTable', () => {
let fixture: ComponentFixture<SimpleCdkTableApp>;
let component: SimpleCdkTableApp;
let dataSource: FakeDataSource;
let table: CdkTable;
let tableElement: HTMLElement;
beforeEach(async(() => {
jasmine.addMatchers(customMatchers);
jasmine.addMatchers(tableCustomMatchers);
TestBed.configureTestingModule({
imports: [CdkDataTableModule],
declarations: [SimpleCdkTableApp],
}).compileComponents();
fixture = TestBed.createComponent(SimpleCdkTableApp);
component = fixture.componentInstance;
dataSource = component.dataSource as FakeDataSource;
table = component.table;
tableElement = fixture.nativeElement.querySelector('cdk-table');
fixture.detectChanges(); // Let the component and table create embedded views
fixture.detectChanges(); // Let the cells render
}));
describe('should initialize', () => {
it('with a connected data source', () => {
expect(table.dataSource).toBe(dataSource);
expect(dataSource.isConnected).toBe(true);
});
it('with a rendered header with the right number of header cells', () => {
const header = getHeaderRow(tableElement);
expect(header).not.toBe(undefined);
expect(header.classList).toContain('customHeaderRowClass');
expect(getHeaderCells(tableElement).length).toBe(component.columnsToRender.length);
});
it('with rendered rows with right number of row cells', () => {
const rows = getRows(tableElement);
expect(rows.length).toBe(dataSource.data.length);
rows.forEach(row => {
expect(row.classList).toContain('customRowClass');
expect(getCells(row).length).toBe(component.columnsToRender.length);
});
});
it('with column class names provided to header and data row cells', () => {
getHeaderCells(tableElement).forEach((headerCell, index) => {
expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
});
getRows(tableElement).forEach(row => {
getCells(row).forEach((cell, index) => {
expect(cell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
});
});
});
it('with the right accessibility roles', () => {
expect(tableElement).toBeRole('grid');
expect(getHeaderRow(tableElement)).toBeRole('row');
getHeaderCells(tableElement).forEach(cell => expect(cell).toBeRole('columnheader'));
getRows(tableElement).forEach(row => {
expect(row).toBeRole('row');
getCells(row).forEach(cell => expect(cell).toBeRole('gridcell'));
});
});
});
it('should re-render the rows when the data changes', () => {
dataSource.addData();
fixture.detectChanges();
expect(getRows(tableElement).length).toBe(dataSource.data.length);
// Check that the number of cells is correct
getRows(tableElement).forEach(row => {
expect(getCells(row).length).toBe(component.columnsToRender.length);
});
});
// TODO(andrewseguin): Add test for dynamic classes on header/rows
it('should match the right table content with dynamic data', () => {
let initialDataLength = dataSource.data.length;
expect(dataSource.data.length).toBe(3);
let headerContent = ['Column A', 'Column B', 'Column C'];
let initialTableContent = [headerContent];
dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
expect(tableElement).toMatchTableContent(initialTableContent);
// Add data to the table and recreate what the rendered output should be.
dataSource.addData();
expect(dataSource.data.length).toBe(initialDataLength + 1); // Make sure data was added
fixture.detectChanges();
fixture.detectChanges();
let changedTableContent = [headerContent];
dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
expect(tableElement).toMatchTableContent(changedTableContent);
});
});
interface TestData {
a: string;
b: string;
c: string;
}
class FakeDataSource extends DataSource<TestData> {
isConnected = false;
_dataChange = new BehaviorSubject<TestData[]>([]);
set data(data: TestData[]) { this._dataChange.next(data); }
get data() { return this._dataChange.getValue(); }
constructor() {
super();
for (let i = 0; i < 3; i++) { this.addData(); }
}
connect(collectionViewer: CollectionViewer): Observable<TestData[]> {
this.isConnected = true;
const streams = [collectionViewer.viewChanged, this._dataChange];
return Observable.combineLatest(streams).map((results: any[]) => {
const [view, data] = results;
return data;
});
}
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: `
<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 class="customHeaderRowClass"
*cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row class="customRowClass"
*cdkRowDef="let row; columns: columnsToRender"></cdk-row>
</cdk-table>
`
})
class SimpleCdkTableApp {
dataSource: FakeDataSource = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(CdkTable) table: CdkTable;
}
function getElements(element: Element, query: string): Element[] {
return [].slice.call(element.querySelectorAll(query));
}
function getHeaderRow(tableElement: Element): Element {
return tableElement.querySelector('.cdk-header-row');
}
function getRows(tableElement: Element): Element[] {
return getElements(tableElement, '.cdk-row');
}
function getCells(row: Element): Element[] {
return row ? getElements(row, '.cdk-cell') : [];
}
function getHeaderCells(tableElement: Element): Element[] {
return getElements(getHeaderRow(tableElement), '.cdk-header-cell');
}
const tableCustomMatchers: jasmine.CustomMatcherFactories = {
toMatchTableContent: function(util, customEqualityTesters) {
return {
compare: function (tableElement: Element, expectedTableContent: any[]) {
const missedExpectations = [];
function checkCellContent(cell: Element, expectedTextContent: string) {
const actualTextContent = cell.textContent.trim();
if (actualTextContent !== expectedTextContent) {
missedExpectations.push(
`Expected cell contents to be ${expectedTextContent} but was ${actualTextContent}`);
}
}
// Check header cells
const expectedHeaderContent = expectedTableContent.shift();
getHeaderCells(tableElement).forEach((cell, index) => {
return checkCellContent(cell, expectedHeaderContent[index]);
});
// Check data row cells
getRows(tableElement).forEach((row, rowIndex) => {
getCells(row).forEach((cell, cellIndex) => {
checkCellContent(cell, expectedTableContent[rowIndex][cellIndex]);
});
});
if (missedExpectations.length) {
return {
pass: false,
message: missedExpectations.join('\n')
};
}
return {
pass: true,
message: 'Table contained the right content'
};
}
};
}
};
+190
View File
@@ -0,0 +1,190 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
Directive,
Input,
QueryList,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/observable/combineLatest';
import {CollectionViewer, DataSource} from './data-source';
import {CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
/**
* 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) { }
}
/**
* A data table that connects with a data source to retrieve data and renders
* a header row and data rows. Updates the rows when new data is provided by the data source.
*/
@Component({
selector: 'cdk-table',
template: `
<ng-container headerRowPlaceholder></ng-container>
<ng-container rowPlaceholder></ng-container>
`,
host: {
'class': 'cdk-table',
'role': 'grid' // TODO(andrewseguin): Allow the user to choose either grid or treegrid
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkTable implements CollectionViewer {
/**
* 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() dataSource: DataSource<any>;
// 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.
*/
viewChanged =
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE});
/**
* Map of all the user's defined columns identified by name.
* Contains the header and data-cell templates.
*/
private _columnDefinitionsByName = new Map<string, CdkColumnDef>();
// 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) _columnDefinitions: QueryList<CdkColumnDef>;
/** Template used as the header container. */
@ContentChild(CdkHeaderRowDef) _headerDefinition: CdkHeaderRowDef;
/** Set of templates that used as the data row containers. */
@ContentChildren(CdkRowDef) _rowDefinitions: QueryList<CdkRowDef>;
constructor(private _changeDetectorRef: ChangeDetectorRef) {
console.warn('The data table is still in active development ' +
'and should be considered unstable.');
}
ngOnDestroy() {
// TODO(andrewseguin): Disconnect from the data source so
// that it can unsubscribe from its streams.
}
ngOnInit() {
// TODO(andrewseguin): Setup a listener for scroll events
// and emit the calculated view to this.viewChanged
}
ngAfterContentInit() {
// TODO(andrewseguin): Throw an error if two columns share the same name
this._columnDefinitions.forEach(columnDef => {
this._columnDefinitionsByName.set(columnDef.name, columnDef);
});
}
ngAfterViewInit() {
// TODO(andrewseguin): Re-render the header when the header's columns change.
this.renderHeaderRow();
// TODO(andrewseguin): Re-render rows when their list of columns change.
// TODO(andrewseguin): If the data source is not
// present after view init, connect it when it is defined.
// TODO(andrewseguin): Unsubscribe from this on destroy.
this.dataSource.connect(this).subscribe((rowsData: any[]) => {
// TODO(andrewseguin): Add a differ that will check if the data has changed,
// rather than re-rendering all rows
this._rowPlaceholder.viewContainer.clear();
rowsData.forEach(rowData => this.insertRow(rowData));
this._changeDetectorRef.markForCheck();
});
}
/**
* Create the embedded view for the header template and place it in the header row view container.
*/
renderHeaderRow() {
const cells = this.getHeaderCellTemplatesForRow(this._headerDefinition);
// TODO(andrewseguin): add some code to enforce that exactly
// one CdkCellOutlet was instantiated as a result
// of `createEmbeddedView`.
this._headerRowPlaceholder.viewContainer
.createEmbeddedView(this._headerDefinition.template, {cells});
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
CdkCellOutlet.mostRecentCellOutlet.context = {};
}
/**
* Create the embedded view for the data row template and place it in the correct index location
* within the data row view container.
*/
insertRow(rowData: any) {
// TODO(andrewseguin): Add when predicates to the row definitions
// to find the right template to used based on
// the data rather than choosing the first row definition.
const row = this._rowDefinitions.first;
// TODO(andrewseguin): Add more context, such as first/last/isEven/etc
const context = {$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);
// Insert empty cells if there is no data to improve rendering time.
CdkCellOutlet.mostRecentCellOutlet.cells = rowData ? this.getCellTemplatesForRow(row) : [];
CdkCellOutlet.mostRecentCellOutlet.context = context;
}
/**
* Returns the cell template definitions to insert into the header
* as defined by its list of columns to display.
*/
getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
return headerDef.columns.map(columnId => {
return this._columnDefinitionsByName.get(columnId).headerCell;
});
}
/**
* Returns the cell template definitions to insert in the provided row
* as defined by its list of columns to display.
*/
getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
return rowDef.columns.map(columnId => {
return this._columnDefinitionsByName.get(columnId).cell;
});
}
}
+24
View File
@@ -0,0 +1,24 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {HeaderRowPlaceholder, RowPlaceholder, CdkTable} from './data-table';
import {CdkCellOutlet, CdkHeaderRow, CdkHeaderRowDef, CdkRow, CdkRowDef} from './row';
import {CdkColumnDef, CdkHeaderCellDef, CdkHeaderCell, CdkCell, CdkCellDef} from './cell';
export * from './data-source';
export * from './data-table';
@NgModule({
imports: [CommonModule],
exports: [
CdkTable, CdkRowDef, CdkCellDef, CdkCellOutlet, CdkHeaderCellDef,
CdkColumnDef, CdkCell, CdkRow,
CdkHeaderCell, CdkHeaderRow, CdkHeaderRowDef],
declarations: [
CdkTable, CdkRowDef, CdkCellDef, CdkCellOutlet, CdkHeaderCellDef,
CdkColumnDef, CdkCell, CdkRow,
CdkHeaderCell, CdkHeaderRow, CdkHeaderRowDef,
RowPlaceholder, HeaderRowPlaceholder,
]
})
export class CdkDataTableModule { }
+90
View File
@@ -0,0 +1,90 @@
import {
ChangeDetectionStrategy,
Component,
Directive,
Input,
TemplateRef,
ViewContainerRef
} from '@angular/core';
import {CdkCellDef} from './cell';
/**
* Header row definition for the CDK data-table.
* Captures the header row's template and other header properties such as the columns to display.
*/
@Directive({selector: '[cdkHeaderRowDef]'})
export class CdkHeaderRowDef {
@Input('cdkHeaderRowDef') columns: string[];
constructor(public template: TemplateRef<any>) { }
}
/**
* Data row definition for the CDK data-table.
* Captures the header row's template and other row properties such as the columns to display.
*/
@Directive({selector: '[cdkRowDef]'})
export class CdkRowDef {
@Input('cdkRowDefColumns') columns: string[];
// TODO(andrewseguin): Add an input for providing a switch function to determine
// if this template should be used.
constructor(public template: TemplateRef<any>) { }
}
/**
* Outlet for rendering cells inside of a row or header row.
* @docs-private
*/
@Directive({selector: '[cdkCellOutlet]'})
export class CdkCellOutlet {
/** The ordered list of cells to render within this outlet's view container */
cells: CdkCellDef[];
/** The data context to be provided to each cell */
context: any;
/**
* Static property containing the latest constructed instance of this class.
* Used by the CDK data-table when each CdkHeaderRow and CdkRow component is created using
* createEmbeddedView. After one of these components are created, this property will provide
* a handle to provide that component's cells and context. After init, the CdkCellOutlet will
* construct the cells with the provided context.
*/
static mostRecentCellOutlet: CdkCellOutlet = null;
constructor(private _viewContainer: ViewContainerRef) {
CdkCellOutlet.mostRecentCellOutlet = this;
}
ngOnInit() {
this.cells.forEach(cell => {
this._viewContainer.createEmbeddedView(cell.template, this.context);
});
}
}
/** Header template container that contains the cell outlet. Adds the right class and role. */
@Component({
selector: 'cdk-header-row',
template: '<ng-container cdkCellOutlet></ng-container>',
host: {
'class': 'cdk-header-row',
'role': 'row',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkHeaderRow { }
/** Data row template container that contains the cell outlet. Adds the right class and role. */
@Component({
selector: 'cdk-row',
template: '<ng-container cdkCellOutlet></ng-container>',
host: {
'class': 'cdk-row',
'role': 'row',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkRow { }
+21
View File
@@ -0,0 +1,21 @@
/**
* Collection of useful custom jasmine matchers for tests.
*/
export const customMatchers: jasmine.CustomMatcherFactories = {
toBeRole: function(util: jasmine.MatchersUtil,
customEqualityTesters: jasmine.CustomEqualityTester[]) {
return {
compare: function (element: Element, expectedRole: string) {
const result: jasmine.CustomMatcherResult = {pass: false};
result.pass = element.getAttribute('role') === expectedRole;
result.message = `Expected role for ${element.tagName} to be ${expectedRole}`;
if (!result.pass) {
result.message += ` but was ${expectedRole}`;
}
return result;
}
};
}
};
+7 -5
View File
@@ -1,13 +1,13 @@
import {NgModule} from '@angular/core';
import {
MdRippleModule,
RtlModule,
ObserveContentModule,
PortalModule,
OverlayModule,
A11yModule,
MdCommonModule,
MdRippleModule,
ObserveContentModule,
OverlayModule,
PortalModule,
RtlModule
} from './core/index';
import {MdButtonToggleModule} from './button-toggle/index';
@@ -36,6 +36,7 @@ import {PlatformModule} from './core/platform/index';
import {MdAutocompleteModule} from './autocomplete/index';
import {StyleModule} from './core/style/index';
import {MdDatepickerModule} from './datepicker/index';
import {CdkDataTableModule} from './core/data-table/index';
const MATERIAL_MODULES = [
MdAutocompleteModule,
@@ -71,6 +72,7 @@ const MATERIAL_MODULES = [
PlatformModule,
MdCommonModule,
ObserveContentModule,
CdkDataTableModule
];
/** @deprecated */
+1
View File
@@ -13,6 +13,7 @@ export * from './button-toggle/index';
export * from './card/index';
export * from './chips/index';
export * from './checkbox/index';
export * from './core/data-table/index';
export * from './datepicker/index';
export * from './dialog/index';
export * from './grid-list/index';
+8
View File
@@ -1 +1,9 @@
declare var module: {id: string};
/** Extends the interface for jasmine matchers to allow for custom matchers. */
declare namespace jasmine {
interface Matchers {
toBeRole(expectedRole: string): boolean;
toMatchTableContent(expectedContent: any[]): boolean;
}
}
+14 -10
View File
@@ -24,26 +24,30 @@ const ROLLUP_GLOBALS = {
'@angular/cdk': 'ng.cdk',
// Rxjs dependencies
'rxjs/Subject': 'Rx',
'rxjs/add/observable/fromEvent': 'Rx.Observable',
'rxjs/add/observable/combineLatest': 'Rx.Observable',
'rxjs/add/observable/forkJoin': 'Rx.Observable',
'rxjs/add/observable/of': 'Rx.Observable',
'rxjs/add/observable/fromEvent': 'Rx.Observable',
'rxjs/add/observable/merge': 'Rx.Observable',
'rxjs/add/observable/of': 'Rx.Observable',
'rxjs/add/observable/throw': 'Rx.Observable',
'rxjs/add/operator/auditTime': 'Rx.Observable.prototype',
'rxjs/add/operator/toPromise': 'Rx.Observable.prototype',
'rxjs/add/operator/map': 'Rx.Observable.prototype',
'rxjs/add/operator/filter': 'Rx.Observable.prototype',
'rxjs/add/operator/do': 'Rx.Observable.prototype',
'rxjs/add/operator/share': 'Rx.Observable.prototype',
'rxjs/add/operator/finally': 'Rx.Observable.prototype',
'rxjs/add/operator/catch': 'Rx.Observable.prototype',
'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype',
'rxjs/add/operator/do': 'Rx.Observable.prototype',
'rxjs/add/operator/filter': 'Rx.Observable.prototype',
'rxjs/add/operator/finally': 'Rx.Observable.prototype',
'rxjs/add/operator/first': 'Rx.Observable.prototype',
'rxjs/add/operator/let': 'Rx.Observable.prototype',
'rxjs/add/operator/map': 'Rx.Observable.prototype',
'rxjs/add/operator/share': 'Rx.Observable.prototype',
'rxjs/add/operator/startWith': 'Rx.Observable.prototype',
'rxjs/add/operator/switchMap': 'Rx.Observable.prototype',
'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype',
'rxjs/add/operator/toPromise': 'Rx.Observable.prototype',
'rxjs/BehaviorSubject': 'Rx',
'rxjs/Observable': 'Rx',
'rxjs/Subject': 'Rx',
'rxjs/Subscription': 'Rx',
};
export type BundleConfig = {