diff --git a/guides/cdk-table.md b/guides/cdk-table.md new file mode 100644 index 000000000000..035c43f10c8c --- /dev/null +++ b/guides/cdk-table.md @@ -0,0 +1,128 @@ +The `` is an unopinionated, customizable data-table with a fully-templated API, dynamic +columns, and an accessible DOM structure. This component acts as the core upon which anyone can +build their own tailored data-table experience. + +The table provides a foundation upon which other features, such as sorting and pagination, can be +built. Because it enforces no opinions on these matters, developers have full control over the +interaction patterns associated with the table. + +For a Material Design styled table, see the documentation for `` which builds on top of +the CDK data-table. + + + +### Using the CDK data-table + +#### Writing your table template + +The first step to writing the data-table template is to define the columns. +A column definition is specified via an `` with the `cdkColumnDef` directive, giving +column a name. Each column definition then further defines both a header-cell template +(`cdkHeaderCellDef`) and a data-cell template (`cdkCellDef`). + + +```html + + User name + {{row.a}} + +``` + +The set of columns defined represent the columns that are _available_ to be rendered. The specific +columns rendered in a given row, and their order, are specified on the row (see below). + +Note that `cdkCellDef` exports the row context such that the row data can be referenced in the cell +template. The directive also exports the same properties as `ngFor` (index, even, odd, first, +last). + +The next step is to define the table's header-row (`cdkHeaderRowDef`) and data-row (`cdkRowDef`). + +```html + + +``` + +These row templates accept the specific columns to be rendered via the name given to the +`cdkColumnDef`. + + +The `cdkRowDef` also exports row context, which can be used for event and property +bindings on the row element. Any content placed _inside_ of the header row or data row template +will be ignored, as the rendered content of the row comes from the cell templates described +above. + + +##### Example: table with three columns +```html + + + + User name + {{row.username}} + + + + + Age + {{row.age}} + + + + + Title + {{row.title}} + + + + + + +``` + +The columns given on the row determine which cells are rendered and in which order. Thus, the +columns can be set via binding to support dynamically changing the columns shown at run-time. + + +It is not required to display all the columns that are defined within the template, +nor use the same ordering. For example, to display the table with only `age` +and `username` and in that order, then the row and header definitions would be written as: + +```html + +``` + +Event and property bindings can be added directly to the row element. + +##### Example: table with event and class binding +```html + + + + = 18” + (click)=”handleRowClick(row)”> + +``` + +#### Connecting the table to a data source +Data is provided to the table through a `DataSource`. When the table receives a data source, +it calls the DataSource's connect function which returns an observable that emits an array of data. +Whenever the data source emits data to this stream, the table will update. + +Because the _data source_ provides this stream, it bears the responsibility of triggering table +updates. This can be based on _anything_: websocket connections, user interaction, model updates, +time-based intervals, etc. Most commonly, updates will be triggered by user interactions like +sorting and pagination. + +##### `trackBy` +To improve performance, a trackBy function can be provided to the table similar to Angular’s +(`ngFor` trackBy)[trackBy]. This informs the table how to uniquely identify rows to track how the +data changes with each update. + +```html + +``` + + +[trackBy][https://angular.io/api/common/NgForOf#change-propagation] \ No newline at end of file diff --git a/src/demo-app/table/table-demo.html b/src/demo-app/table/table-demo.html index 403a04f2b8a8..11d0a5d601ce 100644 --- a/src/demo-app/table/table-demo.html +++ b/src/demo-app/table/table-demo.html @@ -76,8 +76,8 @@

CdkTable Example

{{row.color}}
- - + - @@ -130,8 +130,8 @@

MdTable Example

{{row.color}} - - + +
diff --git a/src/demo-app/table/table-demo.ts b/src/demo-app/table/table-demo.ts index 9bb1050b4fa1..86e6a3301c7c 100644 --- a/src/demo-app/table/table-demo.ts +++ b/src/demo-app/table/table-demo.ts @@ -16,7 +16,7 @@ export type TrackByStrategy = 'id' | 'reference' | 'index'; }) export class TableDemo { dataSource: PersonDataSource | null; - propertiesToDisplay: UserProperties[] = []; + displayedColumns: UserProperties[] = []; trackByStrategy: TrackByStrategy = 'reference'; changeReferences = false; highlights = new Set(); @@ -32,7 +32,7 @@ export class TableDemo { } connect() { - this.propertiesToDisplay = ['userId', 'userName', 'progress', 'color']; + this.displayedColumns = ['userId', 'userName', 'progress', 'color']; this.dataSource = new PersonDataSource(this._peopleDatabase, this._paginator, this.sort); this._peopleDatabase.initialize(); @@ -40,7 +40,7 @@ export class TableDemo { disconnect() { this.dataSource = null; - this.propertiesToDisplay = []; + this.displayedColumns = []; } getOpacity(progress: number) { @@ -57,11 +57,11 @@ export class TableDemo { } toggleColorColumn() { - let colorColumnIndex = this.propertiesToDisplay.indexOf('color'); + let colorColumnIndex = this.displayedColumns.indexOf('color'); if (colorColumnIndex == -1) { - this.propertiesToDisplay.push('color'); + this.displayedColumns.push('color'); } else { - this.propertiesToDisplay.splice(colorColumnIndex, 1); + this.displayedColumns.splice(colorColumnIndex, 1); } } diff --git a/src/lib/table/table.md b/src/lib/table/table.md new file mode 100644 index 000000000000..e70f8fbfc146 --- /dev/null +++ b/src/lib/table/table.md @@ -0,0 +1,49 @@ +The `md-table` provides a Material Design styled data-table that can be used to display rows of +data. + +This table builds on the foundation of the CDK data-table and uses a similar interface for its +data source input and template, except that its element selectors will be prefixed with `md-` +instead of `cdk-`. + + + +Note that the column definition directives (`cdkColumnDef` and `cdkHeaderCellDef`) are still +prefixed with `cdk-`. + +For more information on the interface and how it works, see the guide covering the CDK data-table. + +### Features + +The `` itself only deals with the rendering of a table structure (rows and cells). +Additional features can be built on top of the table by adding behavior inside cell templates +(e.g., sort headers) or next to the table (e.g. a paginator). Interactions that affect the +rendered data (such as sorting and pagination) should be propagated through the table's data source. + + +#### Pagination + +The `` adds a pagination UI that can be used in conjunction with the ``. The +paginator emits events that can be used to trigger an update via the table's data source. + + + +#### Sorting +Use the `mdSort` directive and `` adds a sorting UI the table's column headers. The +sort headers emit events that can be used to trigger an update via the table's data source. + + + +#### Filtering + +While Angular Material does not offer a specific component for filtering tabular data, the table's +data source can be updated based on any custom filter UI. Any filtering pattern need only trigger +an update via the table's data source. + + + + +### Simple Table + +In the near future, we will provide a simplified version of the data-table with an easy-to-use +interface, material styling, array input, and more out-of-the-box features (sorting, pagination, +and selection). \ No newline at end of file diff --git a/src/material-examples/cdk-table-basic/cdk-table-basic-example.css b/src/material-examples/cdk-table-basic/cdk-table-basic-example.css new file mode 100644 index 000000000000..73b5c9b4a454 --- /dev/null +++ b/src/material-examples/cdk-table-basic/cdk-table-basic-example.css @@ -0,0 +1,39 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + min-width: 300px; +} + +/* + * Styles to make the demo's cdk-table match the material design spec + * https://material.io/guidelines/components/data-tables.html + */ +.example-table { + flex: 1 1 auto; + overflow: auto; +} + +.example-header-row, .example-row { + display: flex; + border-bottom: 1px solid #ccc; + align-items: center; + height: 32px; + padding: 0 8px; +} + +.example-cell, .example-header-cell { + flex: 1; +} + +.example-header-cell { + font-size: 12px; + font-weight: bold; + color: rgba(0, 0, 0, 0.54); +} + +.example-cell { + font-size: 13px; + color: rgba(0, 0, 0, 0.87); +} diff --git a/src/material-examples/cdk-table-basic/cdk-table-basic-example.html b/src/material-examples/cdk-table-basic/cdk-table-basic-example.html new file mode 100644 index 000000000000..df20a4d1ed68 --- /dev/null +++ b/src/material-examples/cdk-table-basic/cdk-table-basic-example.html @@ -0,0 +1,36 @@ +
+ + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + + {{row.color}} + + + + + + +
diff --git a/src/material-examples/cdk-table-basic/cdk-table-basic-example.ts b/src/material-examples/cdk-table-basic/cdk-table-basic-example.ts new file mode 100644 index 000000000000..f9b183f9e348 --- /dev/null +++ b/src/material-examples/cdk-table-basic/cdk-table-basic-example.ts @@ -0,0 +1,89 @@ +import {Component} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/map'; + +@Component({ + selector: 'cdk-table-basic-example', + styleUrls: ['cdk-table-basic-example.css'], + templateUrl: 'cdk-table-basic-example.html', +}) +export class CdkTableBasicExample { + displayedColumns = ['userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + dataSource: ExampleDataSource | null; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase); + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + constructor(private _exampleDatabase: ExampleDatabase) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + return this._exampleDatabase.dataChange; + } + + disconnect() {} +} diff --git a/src/material-examples/example-module.ts b/src/material-examples/example-module.ts index 3df3fbf58a05..460d2950ad94 100644 --- a/src/material-examples/example-module.ts +++ b/src/material-examples/example-module.ts @@ -89,8 +89,15 @@ import { MdChipsModule, MdDatepickerModule, MdDialogModule, MdGridListModule, MdIconModule, MdInputModule, MdListModule, MdMenuModule, MdPaginatorModule, MdProgressBarModule, MdProgressSpinnerModule, MdRadioModule, MdSelectModule, MdSidenavModule, MdSliderModule, MdSlideToggleModule, - MdSnackBarModule, MdTabsModule, MdToolbarModule, MdTooltipModule + MdSnackBarModule, MdSortModule, MdTableModule, MdTabsModule, MdToolbarModule, MdTooltipModule } from '@angular/material'; +import {CdkTableModule} from '@angular/cdk'; +import {TableOverviewExample} from './table-overview/table-overview-example'; +import {TablePaginationExample} from './table-pagination/table-pagination-example'; +import {TableBasicExample} from './table-basic/table-basic-example'; +import {TableSortingExample} from './table-sorting/table-sorting-example'; +import {TableFilteringExample} from './table-filtering/table-filtering-example'; +import {CdkTableBasicExample} from './cdk-table-basic/cdk-table-basic-example'; export interface LiveExample { title: string; @@ -114,6 +121,7 @@ export const EXAMPLE_COMPONENTS = { }, 'button-toggle-overview': {title: 'Basic button-toggles', component: ButtonToggleOverviewExample}, 'chips-overview': {title: 'Basic chips', component: ChipsOverviewExample}, + 'cdk-table-basic': {title: 'Basic CDK data-table', component: CdkTableBasicExample}, 'chips-stacked': {title: 'Stacked chips', component: ChipsStackedExample}, 'card-fancy': {title: 'Card with multiple sections', component: CardFancyExample}, 'card-overview': {title: 'Basic cards', component: CardOverviewExample}, @@ -194,6 +202,11 @@ export const EXAMPLE_COMPONENTS = { component: SnackBarComponentExample }, 'snack-bar-overview': {title: 'Basic snack-bar', component: SnackBarOverviewExample}, + 'table-overview': {title: 'Feature-rich data table', component: TableOverviewExample}, + 'table-pagination': {title: 'Table with pagination', component: TablePaginationExample}, + 'table-sorting': {title: 'Table with sorting', component: TableSortingExample}, + 'table-filtering': {title: 'Table with filtering', component: TableFilteringExample}, + 'table-basic': {title: 'Basic table', component: TableBasicExample}, 'tabs-overview': {title: 'Basic tabs', component: TabsOverviewExample}, 'tabs-template-label': {title: 'Coming soon!', component: TabsTemplateLabelExample}, 'toolbar-multirow': {title: 'Multi-row toolbar', component: ToolbarMultirowExample}, @@ -207,6 +220,7 @@ export const EXAMPLE_COMPONENTS = { */ @NgModule({ exports: [ + CdkTableModule, MdAutocompleteModule, MdButtonModule, MdButtonToggleModule, @@ -224,11 +238,13 @@ export const EXAMPLE_COMPONENTS = { MdProgressBarModule, MdProgressSpinnerModule, MdRadioModule, + MdSortModule, MdSelectModule, MdSlideToggleModule, MdSliderModule, MdSidenavModule, MdSnackBarModule, + MdTableModule, MdTabsModule, MdToolbarModule, MdTooltipModule @@ -248,6 +264,7 @@ export const EXAMPLE_LIST = [ ButtonTypesExample, CardFancyExample, CardOverviewExample, + CdkTableBasicExample, ChipsOverviewExample, ChipsStackedExample, CheckboxConfigurableExample, @@ -298,6 +315,11 @@ export const EXAMPLE_LIST = [ SnackBarComponentExample, PizzaPartyComponent, SnackBarOverviewExample, + TableBasicExample, + TableOverviewExample, + TableFilteringExample, + TablePaginationExample, + TableSortingExample, TabsOverviewExample, TabsTemplateLabelExample, ToolbarMultirowExample, diff --git a/src/material-examples/table-basic/table-basic-example.css b/src/material-examples/table-basic/table-basic-example.css new file mode 100644 index 000000000000..7c28e7130685 --- /dev/null +++ b/src/material-examples/table-basic/table-basic-example.css @@ -0,0 +1,20 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + background: white; + min-width: 300px; +} + +.example-header { + min-height: 64px; + display: flex; + align-items: center; + padding-left: 24px; + font-size: 20px; +} + +.mat-table { + overflow: auto; +} diff --git a/src/material-examples/table-basic/table-basic-example.html b/src/material-examples/table-basic/table-basic-example.html new file mode 100644 index 000000000000..159f10561346 --- /dev/null +++ b/src/material-examples/table-basic/table-basic-example.html @@ -0,0 +1,36 @@ +
+
Users
+ + + + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + {{row.color}} + + + + + +
\ No newline at end of file diff --git a/src/material-examples/table-basic/table-basic-example.ts b/src/material-examples/table-basic/table-basic-example.ts new file mode 100644 index 000000000000..1e0d1f88e757 --- /dev/null +++ b/src/material-examples/table-basic/table-basic-example.ts @@ -0,0 +1,89 @@ +import {Component} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/map'; + +@Component({ + selector: 'table-basic-example', + styleUrls: ['table-basic-example.css'], + templateUrl: 'table-basic-example.html', +}) +export class TableBasicExample { + displayedColumns = ['userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + dataSource: ExampleDataSource | null; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase); + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + constructor(private _exampleDatabase: ExampleDatabase) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + return this._exampleDatabase.dataChange; + } + + disconnect() {} +} diff --git a/src/material-examples/table-filtering/table-filtering-example.css b/src/material-examples/table-filtering/table-filtering-example.css new file mode 100644 index 000000000000..806488d90fb1 --- /dev/null +++ b/src/material-examples/table-filtering/table-filtering-example.css @@ -0,0 +1,35 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + background: white; + min-width: 300px; +} + +.example-header { + min-height: 64px; + display: flex; + align-items: center; + padding-left: 24px; + font-size: 20px; +} + +.example-header { + min-height: 64px; + display: flex; + align-items: baseline; + padding: 8px 24px 0; + font-size: 20px; + justify-content: space-between; +} + +.mat-input-container { + font-size: 14px; + flex-grow: 1; + margin-left: 32px; +} + +.mat-table { + overflow: auto; +} diff --git a/src/material-examples/table-filtering/table-filtering-example.html b/src/material-examples/table-filtering/table-filtering-example.html new file mode 100644 index 000000000000..89018a27d804 --- /dev/null +++ b/src/material-examples/table-filtering/table-filtering-example.html @@ -0,0 +1,42 @@ +
+
+ Users + + + + +
+ + + + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + {{row.color}} + + + + + +
\ No newline at end of file diff --git a/src/material-examples/table-filtering/table-filtering-example.ts b/src/material-examples/table-filtering/table-filtering-example.ts new file mode 100644 index 000000000000..ad5226f4fbbc --- /dev/null +++ b/src/material-examples/table-filtering/table-filtering-example.ts @@ -0,0 +1,116 @@ +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {MdSort} from '@angular/material'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/observable/fromEvent'; + +@Component({ + selector: 'table-filtering-example', + styleUrls: ['table-filtering-example.css'], + templateUrl: 'table-filtering-example.html', +}) +export class TableFilteringExample { + displayedColumns = ['userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + dataSource: ExampleDataSource | null; + + @ViewChild('filter') filter: ElementRef; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase); + Observable.fromEvent(this.filter.nativeElement, 'keyup') + .debounceTime(150) + .distinctUntilChanged() + .subscribe(() => { + if (!this.dataSource) { return; } + this.dataSource.filter = this.filter.nativeElement.value; + }); + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + _filterChange = new BehaviorSubject(''); + get filter(): string { return this._filterChange.value; } + set filter(filter: string) { this._filterChange.next(filter); } + + constructor(private _exampleDatabase: ExampleDatabase) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + const displayDataChanges = [ + this._exampleDatabase.dataChange, + this._filterChange, + ]; + + return Observable.merge(...displayDataChanges).map(() => { + return this._exampleDatabase.data.slice().filter((item: UserData) => { + let searchStr = (item.name + item.color).toLowerCase(); + return searchStr.indexOf(this.filter.toLowerCase()) != -1; + }); + }); + } + + disconnect() {} +} diff --git a/src/material-examples/table-overview/table-overview-example.css b/src/material-examples/table-overview/table-overview-example.css new file mode 100644 index 000000000000..3e9ead915745 --- /dev/null +++ b/src/material-examples/table-overview/table-overview-example.css @@ -0,0 +1,57 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + background: white; + min-width: 300px; +} + +.example-header { + min-height: 56px; + max-height: 56px; + display: flex; + align-items: center; + padding: 8px 24px 0; + font-size: 20px; + justify-content: space-between; + border-bottom: 1px solid transparent; +} + +.mat-input-container { + font-size: 14px; + flex-grow: 1; + margin-left: 32px; + margin-top: 8px; +} + +.example-no-results { + display: flex; + justify-content: center; + padding: 24px; + font-size: 12px; + font-style: italic; +} + +/** Selection styles */ +.example-selection-header { + font-size: 18px; + background: rgba(255, 64, 129, 0.3); + border-bottom: 1px solid #d696ac; +} + +.mat-column-select { + max-width: 54px; +} + +.mat-row:hover, .example-selected-row { + background: #f5f5f5; +} + +.mat-row:active, .mat-row.example-selected-row { + background: #eaeaea; +} + +.mat-table { + overflow: auto; +} diff --git a/src/material-examples/table-overview/table-overview-example.html b/src/material-examples/table-overview/table-overview-example.html new file mode 100644 index 000000000000..eeac9604837b --- /dev/null +++ b/src/material-examples/table-overview/table-overview-example.html @@ -0,0 +1,79 @@ +
+
+ Users + + + + +
+
+ {{selection.selected.length}} + {{selection.selected.length == 1 ? 'user' : 'users'}} + selected +
+ + + + + + + + + + + + + + + + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + {{row.color}} + + + + + + + +
+ No users found matching filter. +
+ + + +
diff --git a/src/material-examples/table-overview/table-overview-example.ts b/src/material-examples/table-overview/table-overview-example.ts new file mode 100644 index 000000000000..568aebc5f255 --- /dev/null +++ b/src/material-examples/table-overview/table-overview-example.ts @@ -0,0 +1,181 @@ +import {Component, ElementRef, ViewChild} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {MdPaginator, MdSort, SelectionModel} from '@angular/material'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/debounceTime'; + +@Component({ + selector: 'table-overview-example', + styleUrls: ['table-overview-example.css'], + templateUrl: 'table-overview-example.html', +}) +export class TableOverviewExample { + displayedColumns = ['select', 'userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + selection = new SelectionModel(true, []); + dataSource: ExampleDataSource | null; + + @ViewChild(MdPaginator) paginator: MdPaginator; + @ViewChild(MdSort) sort: MdSort; + @ViewChild('filter') filter: ElementRef; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase, this.paginator, this.sort); + Observable.fromEvent(this.filter.nativeElement, 'keyup') + .debounceTime(150) + .distinctUntilChanged() + .subscribe(() => { + if (!this.dataSource) { return; } + this.dataSource.filter = this.filter.nativeElement.value; + }); + } + + isAllSelected(): boolean { + if (!this.dataSource) { return false; } + if (this.selection.isEmpty()) { return false; } + + if (this.filter.nativeElement.value) { + return this.selection.selected.length == this.dataSource.renderedData.length; + } else { + return this.selection.selected.length == this.exampleDatabase.data.length; + } + } + + masterToggle() { + if (!this.dataSource) { return; } + + if (this.isAllSelected()) { + this.selection.clear(); + } else if (this.filter.nativeElement.value) { + this.dataSource.renderedData.forEach(data => this.selection.select(data.id)); + } else { + this.exampleDatabase.data.forEach(data => this.selection.select(data.id)); + } + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + _filterChange = new BehaviorSubject(''); + get filter(): string { return this._filterChange.value; } + set filter(filter: string) { this._filterChange.next(filter); } + + filteredData: UserData[] = []; + renderedData: UserData[] = []; + + constructor(private _exampleDatabase: ExampleDatabase, + private _paginator: MdPaginator, + private _sort: MdSort) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + // Listen for any changes in the base data, sorting, filtering, or pagination + const displayDataChanges = [ + this._exampleDatabase.dataChange, + this._sort.mdSortChange, + this._filterChange, + this._paginator.page, + ]; + + return Observable.merge(...displayDataChanges).map(() => { + // Filter data + this.filteredData = this._exampleDatabase.data.slice().filter((item: UserData) => { + let searchStr = (item.name + item.color).toLowerCase(); + return searchStr.indexOf(this.filter.toLowerCase()) != -1; + }); + + // Sort filtered data + const sortedData = this.sortData(this.filteredData.slice()); + + // Grab the page's slice of the filtered sorted data. + const startIndex = this._paginator.pageIndex * this._paginator.pageSize; + this.renderedData = sortedData.splice(startIndex, this._paginator.pageSize); + return this.renderedData; + }); + } + + disconnect() {} + + /** Returns a sorted copy of the database data. */ + sortData(data: UserData[]): UserData[] { + if (!this._sort.active || this._sort.direction == '') { return data; } + + return data.sort((a, b) => { + let propertyA: number|string = ''; + let propertyB: number|string = ''; + + switch (this._sort.active) { + case 'userId': [propertyA, propertyB] = [a.id, b.id]; break; + case 'userName': [propertyA, propertyB] = [a.name, b.name]; break; + case 'progress': [propertyA, propertyB] = [a.progress, b.progress]; break; + case 'color': [propertyA, propertyB] = [a.color, b.color]; break; + } + + let valueA = isNaN(+propertyA) ? propertyA : +propertyA; + let valueB = isNaN(+propertyB) ? propertyB : +propertyB; + + return (valueA < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1); + }); + } +} diff --git a/src/material-examples/table-pagination/table-pagination-example.css b/src/material-examples/table-pagination/table-pagination-example.css new file mode 100644 index 000000000000..7c28e7130685 --- /dev/null +++ b/src/material-examples/table-pagination/table-pagination-example.css @@ -0,0 +1,20 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + background: white; + min-width: 300px; +} + +.example-header { + min-height: 64px; + display: flex; + align-items: center; + padding-left: 24px; + font-size: 20px; +} + +.mat-table { + overflow: auto; +} diff --git a/src/material-examples/table-pagination/table-pagination-example.html b/src/material-examples/table-pagination/table-pagination-example.html new file mode 100644 index 000000000000..d6c72ad51e78 --- /dev/null +++ b/src/material-examples/table-pagination/table-pagination-example.html @@ -0,0 +1,43 @@ +
+
Users
+ + + + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + {{row.color}} + + + + + + + + +
\ No newline at end of file diff --git a/src/material-examples/table-pagination/table-pagination-example.ts b/src/material-examples/table-pagination/table-pagination-example.ts new file mode 100644 index 000000000000..730b0437355d --- /dev/null +++ b/src/material-examples/table-pagination/table-pagination-example.ts @@ -0,0 +1,103 @@ +import {Component, ViewChild} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {MdPaginator} from '@angular/material'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/map'; + +@Component({ + selector: 'table-pagination-example', + styleUrls: ['table-pagination-example.css'], + templateUrl: 'table-pagination-example.html', +}) +export class TablePaginationExample { + displayedColumns = ['userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + dataSource: ExampleDataSource | null; + + @ViewChild(MdPaginator) paginator: MdPaginator; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase, this.paginator); + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + constructor(private _exampleDatabase: ExampleDatabase, private _paginator: MdPaginator) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + const displayDataChanges = [ + this._exampleDatabase.dataChange, + this._paginator.page, + ]; + + return Observable.merge(...displayDataChanges).map(() => { + const data = this._exampleDatabase.data.slice(); + + // Grab the page's slice of data. + const startIndex = this._paginator.pageIndex * this._paginator.pageSize; + return data.splice(startIndex, this._paginator.pageSize); + }); + } + + disconnect() {} +} diff --git a/src/material-examples/table-sorting/table-sorting-example.css b/src/material-examples/table-sorting/table-sorting-example.css new file mode 100644 index 000000000000..8cd1462a1da1 --- /dev/null +++ b/src/material-examples/table-sorting/table-sorting-example.css @@ -0,0 +1,24 @@ +/* Structure */ +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + background: white; + min-width: 300px; +} + +.example-header { + min-height: 64px; + display: flex; + align-items: center; + padding-left: 24px; + font-size: 20px; +} + +.mat-table { + overflow: auto; +} + +.mat-header-cell .mat-sort-header-sorted { + color: black; +} \ No newline at end of file diff --git a/src/material-examples/table-sorting/table-sorting-example.html b/src/material-examples/table-sorting/table-sorting-example.html new file mode 100644 index 000000000000..0b304c7e67e8 --- /dev/null +++ b/src/material-examples/table-sorting/table-sorting-example.html @@ -0,0 +1,36 @@ +
+
Users
+ + + + + + + + ID + {{row.id}} + + + + + Progress + {{row.progress}}% + + + + + Name + {{row.name}} + + + + + Color + {{row.color}} + + + + + +
\ No newline at end of file diff --git a/src/material-examples/table-sorting/table-sorting-example.ts b/src/material-examples/table-sorting/table-sorting-example.ts new file mode 100644 index 000000000000..9578c59b1172 --- /dev/null +++ b/src/material-examples/table-sorting/table-sorting-example.ts @@ -0,0 +1,122 @@ +import {Component, ViewChild} from '@angular/core'; +import {DataSource} from '@angular/cdk'; +import {MdSort} from '@angular/material'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/startWith'; +import 'rxjs/add/observable/merge'; +import 'rxjs/add/operator/map'; + +@Component({ + selector: 'table-sorting-example', + styleUrls: ['table-sorting-example.css'], + templateUrl: 'table-sorting-example.html', +}) +export class TableSortingExample { + displayedColumns = ['userId', 'userName', 'progress', 'color']; + exampleDatabase = new ExampleDatabase(); + dataSource: ExampleDataSource | null; + + @ViewChild(MdSort) sort: MdSort; + + ngOnInit() { + this.dataSource = new ExampleDataSource(this.exampleDatabase, this.sort); + } +} + +/** Constants used to fill up our data base. */ +const COLORS = ['maroon', 'red', 'orange', 'yellow', 'olive', 'green', 'purple', + 'fuchsia', 'lime', 'teal', 'aqua', 'blue', 'navy', 'black', 'gray']; +const NAMES = ['Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack', + 'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper', + 'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas', 'Elizabeth']; + +export interface UserData { + id: string; + name: string; + progress: string; + color: string; +} + +/** An example database that the data source uses to retrieve data for the table. */ +export class ExampleDatabase { + /** Stream that emits whenever the data has been modified. */ + dataChange: BehaviorSubject = new BehaviorSubject([]); + get data(): UserData[] { return this.dataChange.value; } + + constructor() { + // Fill up the database with 100 users. + for (let i = 0; i < 100; i++) { this.addUser(); } + } + + /** Adds a new user to the database. */ + addUser() { + const copiedData = this.data.slice(); + copiedData.push(this.createNewUser()); + this.dataChange.next(copiedData); + } + + /** Builds and returns a new User. */ + private createNewUser() { + const name = + NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' + + NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.'; + + return { + id: (this.data.length + 1).toString(), + name: name, + progress: Math.round(Math.random() * 100).toString(), + color: COLORS[Math.round(Math.random() * (COLORS.length - 1))] + }; + } +} + +/** + * Data source to provide what data should be rendered in the table. Note that the data source + * can retrieve its data in any way. In this case, the data source is provided a reference + * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage + * the underlying data. Instead, it only needs to take the data and send the table exactly what + * should be rendered. + */ +export class ExampleDataSource extends DataSource { + constructor(private _exampleDatabase: ExampleDatabase, private _sort: MdSort) { + super(); + } + + /** Connect function called by the table to retrieve one stream containing the data to render. */ + connect(): Observable { + const displayDataChanges = [ + this._exampleDatabase.dataChange, + this._sort.mdSortChange, + ]; + + return Observable.merge(...displayDataChanges).map(() => { + return this.getSortedData(); + }); + } + + disconnect() {} + + /** Returns a sorted copy of the database data. */ + getSortedData(): UserData[] { + const data = this._exampleDatabase.data.slice(); + if (!this._sort.active || this._sort.direction == '') { return data; } + + return data.sort((a, b) => { + let propertyA: number|string = ''; + let propertyB: number|string = ''; + + switch (this._sort.active) { + case 'userId': [propertyA, propertyB] = [a.id, b.id]; break; + case 'userName': [propertyA, propertyB] = [a.name, b.name]; break; + case 'progress': [propertyA, propertyB] = [a.progress, b.progress]; break; + case 'color': [propertyA, propertyB] = [a.color, b.color]; break; + } + + let valueA = isNaN(+propertyA) ? propertyA : +propertyA; + let valueB = isNaN(+propertyB) ? propertyB : +propertyB; + + return (valueA < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1); + }); + } +} diff --git a/src/material-examples/tsconfig-build.json b/src/material-examples/tsconfig-build.json index 56a129b4ab81..fca6a83f5b30 100644 --- a/src/material-examples/tsconfig-build.json +++ b/src/material-examples/tsconfig-build.json @@ -20,7 +20,8 @@ "types": [], "baseUrl": ".", "paths": { - "@angular/material": ["../../dist/packages/material/public_api"] + "@angular/material": ["../../dist/packages/material/public_api"], + "@angular/cdk": ["../../dist/packages/cdk/public_api"] } }, "files": [ diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index bd158374ca9d..846869fa8465 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -3,7 +3,7 @@ import {createPackageBuildTasks} from 'material2-build-tools'; // Create gulp tasks to build the different packages in the project. createPackageBuildTasks('cdk'); createPackageBuildTasks('material', ['cdk']); -createPackageBuildTasks('material-examples', ['material']); +createPackageBuildTasks('material-examples', ['material', 'cdk']); import './tasks/ci'; import './tasks/clean'; diff --git a/tools/gulp/tasks/docs.ts b/tools/gulp/tasks/docs.ts index 1ae20de8d9af..840ddbc17277 100644 --- a/tools/gulp/tasks/docs.ts +++ b/tools/gulp/tasks/docs.ts @@ -69,7 +69,7 @@ task('docs', [ /** Generates html files from the markdown overviews and guides. */ task('markdown-docs', () => { - return src(['src/lib/**/*.md', 'guides/*.md']) + return src(['src/lib/**/*.md', 'src/cdk/**/*.md', 'guides/*.md']) .pipe(markdown({ // Add syntax highlight using highlight.js highlight: (code: string, language: string) => {