diff --git a/src/demo-app/data-table/data-table-demo.html b/src/demo-app/data-table/data-table-demo.html
new file mode 100644
index 00000000000..bbaab9e33c2
--- /dev/null
+++ b/src/demo-app/data-table/data-table-demo.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ ID
+ {{row.id}}
+
+
+
+
+ Progress
+
+ {{row.progress}}%
+
+
50 ? 'green' : 'red'"
+ [style.opacity]="getOpacity(row.progress)"
+ [style.width.%]="row.progress">
+
+
+
+
+
+
+ Name
+ {{row.name}}
+
+
+
+
+ Color
+ {{row.color}}
+
+
+
+
+
+
diff --git a/src/demo-app/data-table/data-table-demo.scss b/src/demo-app/data-table/data-table-demo.scss
new file mode 100644
index 00000000000..bfbf160f52e
--- /dev/null
+++ b/src/demo-app/data-table/data-table-demo.scss
@@ -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;
+ }
+}
diff --git a/src/demo-app/data-table/data-table-demo.ts b/src/demo-app/data-table/data-table-demo.ts
new file mode 100644
index 00000000000..f72e6fb232f
--- /dev/null
+++ b/src/demo-app/data-table/data-table-demo.ts
@@ -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;
+ }
+}
diff --git a/src/demo-app/data-table/people-database.ts b/src/demo-app/data-table/people-database.ts
new file mode 100644
index 00000000000..a11d6873361
--- /dev/null
+++ b/src/demo-app/data-table/people-database.ts
@@ -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))]
+ });
+ }
+}
diff --git a/src/demo-app/data-table/person-data-source.ts b/src/demo-app/data-table/person-data-source.ts
new file mode 100644
index 00000000000..c21eade02d4
--- /dev/null
+++ b/src/demo-app/data-table/person-data-source.ts
@@ -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 {
+ _renderedData: any[] = [];
+
+ constructor(private _peopleDatabase: PeopleDatabase) {
+ super();
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable {
+ 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;
+ });
+ }
+}
diff --git a/src/demo-app/dataset/colors.ts b/src/demo-app/dataset/colors.ts
new file mode 100644
index 00000000000..05a0516fa8b
--- /dev/null
+++ b/src/demo-app/dataset/colors.ts
@@ -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'
+];
diff --git a/src/demo-app/dataset/names.ts b/src/demo-app/dataset/names.ts
new file mode 100644
index 00000000000..22e7e8c8343
--- /dev/null
+++ b/src/demo-app/dataset/names.ts
@@ -0,0 +1,1002 @@
+export const NAMES: string[] = [
+ 'Abbott',
+ 'Acevedo',
+ 'Acosta',
+ 'Adams',
+ 'Adkins',
+ 'Aguilar',
+ 'Aguirre',
+ 'Albert',
+ 'Alexander',
+ 'Alford',
+ 'Allen',
+ 'Allison',
+ 'Alston',
+ 'Alvarado',
+ 'Alvarez',
+ 'Anderson',
+ 'Andrews',
+ 'Anthony',
+ 'Armstrong',
+ 'Arnold',
+ 'Ashley',
+ 'Atkins',
+ 'Atkinson',
+ 'Austin',
+ 'Avery',
+ 'Avila',
+ 'Ayala',
+ 'Ayers',
+ 'Bailey',
+ 'Baird',
+ 'Baker',
+ 'Baldwin',
+ 'Ball',
+ 'Ballard',
+ 'Banks',
+ 'Barber',
+ 'Barker',
+ 'Barlow',
+ 'Barnes',
+ 'Barnett',
+ 'Barr',
+ 'Barrera',
+ 'Barrett',
+ 'Barron',
+ 'Barry',
+ 'Bartlett',
+ 'Barton',
+ 'Bass',
+ 'Bates',
+ 'Battle',
+ 'Bauer',
+ 'Baxter',
+ 'Beach',
+ 'Bean',
+ 'Beard',
+ 'Beasley',
+ 'Beck',
+ 'Becker',
+ 'Bell',
+ 'Bender',
+ 'Benjamin',
+ 'Bennett',
+ 'Benson',
+ 'Bentley',
+ 'Benton',
+ 'Berg',
+ 'Berger',
+ 'Bernard',
+ 'Berry',
+ 'Best',
+ 'Bird',
+ 'Bishop',
+ 'Black',
+ 'Blackburn',
+ 'Blackwell',
+ 'Blair',
+ 'Blake',
+ 'Blanchard',
+ 'Blankenship',
+ 'Blevins',
+ 'Bolton',
+ 'Bond',
+ 'Bonner',
+ 'Booker',
+ 'Boone',
+ 'Booth',
+ 'Bowen',
+ 'Bowers',
+ 'Bowman',
+ 'Boyd',
+ 'Boyer',
+ 'Boyle',
+ 'Bradford',
+ 'Bradley',
+ 'Bradshaw',
+ 'Brady',
+ 'Branch',
+ 'Bray',
+ 'Brennan',
+ 'Brewer',
+ 'Bridges',
+ 'Briggs',
+ 'Bright',
+ 'Britt',
+ 'Brock',
+ 'Brooks',
+ 'Brown',
+ 'Browning',
+ 'Bruce',
+ 'Bryan',
+ 'Bryant',
+ 'Buchanan',
+ 'Buck',
+ 'Buckley',
+ 'Buckner',
+ 'Bullock',
+ 'Burch',
+ 'Burgess',
+ 'Burke',
+ 'Burks',
+ 'Burnett',
+ 'Burns',
+ 'Burris',
+ 'Burt',
+ 'Burton',
+ 'Bush',
+ 'Butler',
+ 'Byers',
+ 'Byrd',
+ 'Cabrera',
+ 'Cain',
+ 'Calderon',
+ 'Caldwell',
+ 'Calhoun',
+ 'Callahan',
+ 'Camacho',
+ 'Cameron',
+ 'Campbell',
+ 'Campos',
+ 'Cannon',
+ 'Cantrell',
+ 'Cantu',
+ 'Cardenas',
+ 'Carey',
+ 'Carlson',
+ 'Carney',
+ 'Carpenter',
+ 'Carr',
+ 'Carrillo',
+ 'Carroll',
+ 'Carson',
+ 'Carter',
+ 'Carver',
+ 'Case',
+ 'Casey',
+ 'Cash',
+ 'Castaneda',
+ 'Castillo',
+ 'Castro',
+ 'Cervantes',
+ 'Chambers',
+ 'Chan',
+ 'Chandler',
+ 'Chaney',
+ 'Chang',
+ 'Chapman',
+ 'Charles',
+ 'Chase',
+ 'Chavez',
+ 'Chen',
+ 'Cherry',
+ 'Christensen',
+ 'Christian',
+ 'Church',
+ 'Clark',
+ 'Clarke',
+ 'Clay',
+ 'Clayton',
+ 'Clements',
+ 'Clemons',
+ 'Cleveland',
+ 'Cline',
+ 'Cobb',
+ 'Cochran',
+ 'Coffey',
+ 'Cohen',
+ 'Cole',
+ 'Coleman',
+ 'Collier',
+ 'Collins',
+ 'Colon',
+ 'Combs',
+ 'Compton',
+ 'Conley',
+ 'Conner',
+ 'Conrad',
+ 'Contreras',
+ 'Conway',
+ 'Cook',
+ 'Cooke',
+ 'Cooley',
+ 'Cooper',
+ 'Copeland',
+ 'Cortez',
+ 'Cote',
+ 'Cotton',
+ 'Cox',
+ 'Craft',
+ 'Craig',
+ 'Crane',
+ 'Crawford',
+ 'Crosby',
+ 'Cross',
+ 'Cruz',
+ 'Cummings',
+ 'Cunningham',
+ 'Curry',
+ 'Curtis',
+ 'Dale',
+ 'Dalton',
+ 'Daniel',
+ 'Daniels',
+ 'Daugherty',
+ 'Davenport',
+ 'David',
+ 'Davidson',
+ 'Davis',
+ 'Dawson',
+ 'Day',
+ 'Dean',
+ 'Decker',
+ 'Dejesus',
+ 'Delacruz',
+ 'Delaney',
+ 'Deleon',
+ 'Delgado',
+ 'Dennis',
+ 'Diaz',
+ 'Dickerson',
+ 'Dickson',
+ 'Dillard',
+ 'Dillon',
+ 'Dixon',
+ 'Dodson',
+ 'Dominguez',
+ 'Donaldson',
+ 'Donovan',
+ 'Dorsey',
+ 'Dotson',
+ 'Douglas',
+ 'Downs',
+ 'Doyle',
+ 'Drake',
+ 'Dudley',
+ 'Duffy',
+ 'Duke',
+ 'Duncan',
+ 'Dunlap',
+ 'Dunn',
+ 'Duran',
+ 'Durham',
+ 'Dyer',
+ 'Eaton',
+ 'Edwards',
+ 'Elliott',
+ 'Ellis',
+ 'Ellison',
+ 'Emerson',
+ 'England',
+ 'English',
+ 'Erickson',
+ 'Espinoza',
+ 'Estes',
+ 'Estrada',
+ 'Evans',
+ 'Everett',
+ 'Ewing',
+ 'Farley',
+ 'Farmer',
+ 'Farrell',
+ 'Faulkner',
+ 'Ferguson',
+ 'Fernandez',
+ 'Ferrell',
+ 'Fields',
+ 'Figueroa',
+ 'Finch',
+ 'Finley',
+ 'Fischer',
+ 'Fisher',
+ 'Fitzgerald',
+ 'Fitzpatrick',
+ 'Fleming',
+ 'Fletcher',
+ 'Flores',
+ 'Flowers',
+ 'Floyd',
+ 'Flynn',
+ 'Foley',
+ 'Forbes',
+ 'Ford',
+ 'Foreman',
+ 'Foster',
+ 'Fowler',
+ 'Fox',
+ 'Francis',
+ 'Franco',
+ 'Frank',
+ 'Franklin',
+ 'Franks',
+ 'Frazier',
+ 'Frederick',
+ 'Freeman',
+ 'French',
+ 'Frost',
+ 'Fry',
+ 'Frye',
+ 'Fuentes',
+ 'Fuller',
+ 'Fulton',
+ 'Gaines',
+ 'Gallagher',
+ 'Gallegos',
+ 'Galloway',
+ 'Gamble',
+ 'Garcia',
+ 'Gardner',
+ 'Garner',
+ 'Garrett',
+ 'Garrison',
+ 'Garza',
+ 'Gates',
+ 'Gay',
+ 'Gentry',
+ 'George',
+ 'Gibbs',
+ 'Gibson',
+ 'Gilbert',
+ 'Giles',
+ 'Gill',
+ 'Gillespie',
+ 'Gilliam',
+ 'Gilmore',
+ 'Glass',
+ 'Glenn',
+ 'Glover',
+ 'Goff',
+ 'Golden',
+ 'Gomez',
+ 'Gonzales',
+ 'Gonzalez',
+ 'Good',
+ 'Goodman',
+ 'Goodwin',
+ 'Gordon',
+ 'Gould',
+ 'Graham',
+ 'Grant',
+ 'Graves',
+ 'Gray',
+ 'Green',
+ 'Greene',
+ 'Greer',
+ 'Gregory',
+ 'Griffin',
+ 'Griffith',
+ 'Grimes',
+ 'Gross',
+ 'Guerra',
+ 'Guerrero',
+ 'Guthrie',
+ 'Gutierrez',
+ 'Guy',
+ 'Guzman',
+ 'Hahn',
+ 'Hale',
+ 'Haley',
+ 'Hall',
+ 'Hamilton',
+ 'Hammond',
+ 'Hampton',
+ 'Hancock',
+ 'Haney',
+ 'Hansen',
+ 'Hanson',
+ 'Hardin',
+ 'Harding',
+ 'Hardy',
+ 'Harmon',
+ 'Harper',
+ 'Harrell',
+ 'Harrington',
+ 'Harris',
+ 'Harrison',
+ 'Hart',
+ 'Hartman',
+ 'Harvey',
+ 'Hatfield',
+ 'Hawkins',
+ 'Hayden',
+ 'Hayes',
+ 'Haynes',
+ 'Hays',
+ 'Head',
+ 'Heath',
+ 'Hebert',
+ 'Henderson',
+ 'Hendricks',
+ 'Hendrix',
+ 'Henry',
+ 'Hensley',
+ 'Henson',
+ 'Herman',
+ 'Hernandez',
+ 'Herrera',
+ 'Herring',
+ 'Hess',
+ 'Hester',
+ 'Hewitt',
+ 'Hickman',
+ 'Hicks',
+ 'Higgins',
+ 'Hill',
+ 'Hines',
+ 'Hinton',
+ 'Hobbs',
+ 'Hodge',
+ 'Hodges',
+ 'Hoffman',
+ 'Hogan',
+ 'Holcomb',
+ 'Holden',
+ 'Holder',
+ 'Holland',
+ 'Holloway',
+ 'Holman',
+ 'Holmes',
+ 'Holt',
+ 'Hood',
+ 'Hooper',
+ 'Hoover',
+ 'Hopkins',
+ 'Hopper',
+ 'Horn',
+ 'Horne',
+ 'Horton',
+ 'House',
+ 'Houston',
+ 'Howard',
+ 'Howe',
+ 'Howell',
+ 'Hubbard',
+ 'Huber',
+ 'Hudson',
+ 'Huff',
+ 'Huffman',
+ 'Hughes',
+ 'Hull',
+ 'Humphrey',
+ 'Hunt',
+ 'Hunter',
+ 'Hurley',
+ 'Hurst',
+ 'Hutchinson',
+ 'Hyde',
+ 'Ingram',
+ 'Irwin',
+ 'Jackson',
+ 'Jacobs',
+ 'Jacobson',
+ 'James',
+ 'Jarvis',
+ 'Jefferson',
+ 'Jenkins',
+ 'Jennings',
+ 'Jensen',
+ 'Jimenez',
+ 'Johns',
+ 'Johnson',
+ 'Johnston',
+ 'Jones',
+ 'Jordan',
+ 'Joseph',
+ 'Joyce',
+ 'Joyner',
+ 'Juarez',
+ 'Justice',
+ 'Kane',
+ 'Kaufman',
+ 'Keith',
+ 'Keller',
+ 'Kelley',
+ 'Kelly',
+ 'Kemp',
+ 'Kennedy',
+ 'Kent',
+ 'Kerr',
+ 'Key',
+ 'Kidd',
+ 'Kim',
+ 'King',
+ 'Kinney',
+ 'Kirby',
+ 'Kirk',
+ 'Kirkland',
+ 'Klein',
+ 'Kline',
+ 'Knapp',
+ 'Knight',
+ 'Knowles',
+ 'Knox',
+ 'Koch',
+ 'Kramer',
+ 'Lamb',
+ 'Lambert',
+ 'Lancaster',
+ 'Landry',
+ 'Lane',
+ 'Lang',
+ 'Langley',
+ 'Lara',
+ 'Larsen',
+ 'Larson',
+ 'Lawrence',
+ 'Lawson',
+ 'Le',
+ 'Leach',
+ 'Leblanc',
+ 'Lee',
+ 'Leon',
+ 'Leonard',
+ 'Lester',
+ 'Levine',
+ 'Levy',
+ 'Lewis',
+ 'Lindsay',
+ 'Lindsey',
+ 'Little',
+ 'Livingston',
+ 'Lloyd',
+ 'Logan',
+ 'Long',
+ 'Lopez',
+ 'Lott',
+ 'Love',
+ 'Lowe',
+ 'Lowery',
+ 'Lucas',
+ 'Luna',
+ 'Lynch',
+ 'Lynn',
+ 'Lyons',
+ 'Macdonald',
+ 'Macias',
+ 'Mack',
+ 'Madden',
+ 'Maddox',
+ 'Maldonado',
+ 'Malone',
+ 'Mann',
+ 'Manning',
+ 'Marks',
+ 'Marquez',
+ 'Marsh',
+ 'Marshall',
+ 'Martin',
+ 'Martinez',
+ 'Mason',
+ 'Massey',
+ 'Mathews',
+ 'Mathis',
+ 'Matthews',
+ 'Maxwell',
+ 'May',
+ 'Mayer',
+ 'Maynard',
+ 'Mayo',
+ 'Mays',
+ 'Mcbride',
+ 'Mccall',
+ 'Mccarthy',
+ 'Mccarty',
+ 'Mcclain',
+ 'Mcclure',
+ 'Mcconnell',
+ 'Mccormick',
+ 'Mccoy',
+ 'Mccray',
+ 'Mccullough',
+ 'Mcdaniel',
+ 'Mcdonald',
+ 'Mcdowell',
+ 'Mcfadden',
+ 'Mcfarland',
+ 'Mcgee',
+ 'Mcgowan',
+ 'Mcguire',
+ 'Mcintosh',
+ 'Mcintyre',
+ 'Mckay',
+ 'Mckee',
+ 'Mckenzie',
+ 'Mckinney',
+ 'Mcknight',
+ 'Mclaughlin',
+ 'Mclean',
+ 'Mcleod',
+ 'Mcmahon',
+ 'Mcmillan',
+ 'Mcneil',
+ 'Mcpherson',
+ 'Meadows',
+ 'Medina',
+ 'Mejia',
+ 'Melendez',
+ 'Melton',
+ 'Mendez',
+ 'Mendoza',
+ 'Mercado',
+ 'Mercer',
+ 'Merrill',
+ 'Merritt',
+ 'Meyer',
+ 'Meyers',
+ 'Michael',
+ 'Middleton',
+ 'Miles',
+ 'Miller',
+ 'Mills',
+ 'Miranda',
+ 'Mitchell',
+ 'Molina',
+ 'Monroe',
+ 'Montgomery',
+ 'Montoya',
+ 'Moody',
+ 'Moon',
+ 'Mooney',
+ 'Moore',
+ 'Morales',
+ 'Moran',
+ 'Moreno',
+ 'Morgan',
+ 'Morin',
+ 'Morris',
+ 'Morrison',
+ 'Morrow',
+ 'Morse',
+ 'Morton',
+ 'Moses',
+ 'Mosley',
+ 'Moss',
+ 'Mueller',
+ 'Mullen',
+ 'Mullins',
+ 'Munoz',
+ 'Murphy',
+ 'Murray',
+ 'Myers',
+ 'Nash',
+ 'Navarro',
+ 'Neal',
+ 'Nelson',
+ 'Newman',
+ 'Newton',
+ 'Nguyen',
+ 'Nichols',
+ 'Nicholson',
+ 'Nielsen',
+ 'Nieves',
+ 'Nixon',
+ 'Noble',
+ 'Noel',
+ 'Nolan',
+ 'Norman',
+ 'Norris',
+ 'Norton',
+ 'Nunez',
+ 'Obrien',
+ 'Ochoa',
+ 'Oconnor',
+ 'Odom',
+ 'Odonnell',
+ 'Oliver',
+ 'Olsen',
+ 'Olson',
+ 'Oneal',
+ 'Oneil',
+ 'Oneill',
+ 'Orr',
+ 'Ortega',
+ 'Ortiz',
+ 'Osborn',
+ 'Osborne',
+ 'Owen',
+ 'Owens',
+ 'Pace',
+ 'Pacheco',
+ 'Padilla',
+ 'Page',
+ 'Palmer',
+ 'Park',
+ 'Parker',
+ 'Parks',
+ 'Parrish',
+ 'Parsons',
+ 'Pate',
+ 'Patel',
+ 'Patrick',
+ 'Patterson',
+ 'Patton',
+ 'Paul',
+ 'Payne',
+ 'Pearson',
+ 'Peck',
+ 'Pena',
+ 'Pennington',
+ 'Perez',
+ 'Perkins',
+ 'Perry',
+ 'Peters',
+ 'Petersen',
+ 'Peterson',
+ 'Petty',
+ 'Phelps',
+ 'Phillips',
+ 'Pickett',
+ 'Pierce',
+ 'Pittman',
+ 'Pitts',
+ 'Pollard',
+ 'Poole',
+ 'Pope',
+ 'Porter',
+ 'Potter',
+ 'Potts',
+ 'Powell',
+ 'Powers',
+ 'Pratt',
+ 'Preston',
+ 'Price',
+ 'Prince',
+ 'Pruitt',
+ 'Puckett',
+ 'Pugh',
+ 'Quinn',
+ 'Ramirez',
+ 'Ramos',
+ 'Ramsey',
+ 'Randall',
+ 'Randolph',
+ 'Rasmussen',
+ 'Ratliff',
+ 'Ray',
+ 'Raymond',
+ 'Reed',
+ 'Reese',
+ 'Reeves',
+ 'Reid',
+ 'Reilly',
+ 'Reyes',
+ 'Reynolds',
+ 'Rhodes',
+ 'Rice',
+ 'Rich',
+ 'Richard',
+ 'Richards',
+ 'Richardson',
+ 'Richmond',
+ 'Riddle',
+ 'Riggs',
+ 'Riley',
+ 'Rios',
+ 'Rivas',
+ 'Rivera',
+ 'Rivers',
+ 'Roach',
+ 'Robbins',
+ 'Roberson',
+ 'Roberts',
+ 'Robertson',
+ 'Robinson',
+ 'Robles',
+ 'Rocha',
+ 'Rodgers',
+ 'Rodriguez',
+ 'Rodriquez',
+ 'Rogers',
+ 'Rojas',
+ 'Rollins',
+ 'Roman',
+ 'Romero',
+ 'Rosa',
+ 'Rosales',
+ 'Rosario',
+ 'Rose',
+ 'Ross',
+ 'Roth',
+ 'Rowe',
+ 'Rowland',
+ 'Roy',
+ 'Ruiz',
+ 'Rush',
+ 'Russell',
+ 'Russo',
+ 'Rutledge',
+ 'Ryan',
+ 'Salas',
+ 'Salazar',
+ 'Salinas',
+ 'Sampson',
+ 'Sanchez',
+ 'Sanders',
+ 'Sandoval',
+ 'Sanford',
+ 'Santana',
+ 'Santiago',
+ 'Santos',
+ 'Sargent',
+ 'Saunders',
+ 'Savage',
+ 'Sawyer',
+ 'Schmidt',
+ 'Schneider',
+ 'Schroeder',
+ 'Schultz',
+ 'Schwartz',
+ 'Scott',
+ 'Sears',
+ 'Sellers',
+ 'Serrano',
+ 'Sexton',
+ 'Shaffer',
+ 'Shannon',
+ 'Sharp',
+ 'Sharpe',
+ 'Shaw',
+ 'Shelton',
+ 'Shepard',
+ 'Shepherd',
+ 'Sheppard',
+ 'Sherman',
+ 'Shields',
+ 'Short',
+ 'Silva',
+ 'Simmons',
+ 'Simon',
+ 'Simpson',
+ 'Sims',
+ 'Singleton',
+ 'Skinner',
+ 'Slater',
+ 'Sloan',
+ 'Small',
+ 'Smith',
+ 'Snider',
+ 'Snow',
+ 'Snyder',
+ 'Solis',
+ 'Solomon',
+ 'Sosa',
+ 'Soto',
+ 'Sparks',
+ 'Spears',
+ 'Spence',
+ 'Spencer',
+ 'Stafford',
+ 'Stanley',
+ 'Stanton',
+ 'Stark',
+ 'Steele',
+ 'Stein',
+ 'Stephens',
+ 'Stephenson',
+ 'Stevens',
+ 'Stevenson',
+ 'Stewart',
+ 'Stokes',
+ 'Stone',
+ 'Stout',
+ 'Strickland',
+ 'Strong',
+ 'Stuart',
+ 'Suarez',
+ 'Sullivan',
+ 'Summers',
+ 'Sutton',
+ 'Swanson',
+ 'Sweeney',
+ 'Sweet',
+ 'Sykes',
+ 'Talley',
+ 'Tanner',
+ 'Tate',
+ 'Taylor',
+ 'Terrell',
+ 'Terry',
+ 'Thomas',
+ 'Thompson',
+ 'Thornton',
+ 'Tillman',
+ 'Todd',
+ 'Torres',
+ 'Townsend',
+ 'Tran',
+ 'Travis',
+ 'Trevino',
+ 'Trujillo',
+ 'Tucker',
+ 'Turner',
+ 'Tyler',
+ 'Tyson',
+ 'Underwood',
+ 'Valdez',
+ 'Valencia',
+ 'Valentine',
+ 'Valenzuela',
+ 'Vance',
+ 'Vang',
+ 'Vargas',
+ 'Vasquez',
+ 'Vaughan',
+ 'Vaughn',
+ 'Vazquez',
+ 'Vega',
+ 'Velasquez',
+ 'Velazquez',
+ 'Velez',
+ 'Villarreal',
+ 'Vincent',
+ 'Vinson',
+ 'Wade',
+ 'Wagner',
+ 'Walker',
+ 'Wall',
+ 'Wallace',
+ 'Waller',
+ 'Walls',
+ 'Walsh',
+ 'Walter',
+ 'Walters',
+ 'Walton',
+ 'Ward',
+ 'Ware',
+ 'Warner',
+ 'Warren',
+ 'Washington',
+ 'Waters',
+ 'Watkins',
+ 'Watson',
+ 'Watts',
+ 'Weaver',
+ 'Webb',
+ 'Weber',
+ 'Webster',
+ 'Weeks',
+ 'Weiss',
+ 'Welch',
+ 'Wells',
+ 'West',
+ 'Wheeler',
+ 'Whitaker',
+ 'White',
+ 'Whitehead',
+ 'Whitfield',
+ 'Whitley',
+ 'Whitney',
+ 'Wiggins',
+ 'Wilcox',
+ 'Wilder',
+ 'Wiley',
+ 'Wilkerson',
+ 'Wilkins',
+ 'Wilkinson',
+ 'William',
+ 'Williams',
+ 'Williamson',
+ 'Willis',
+ 'Wilson',
+ 'Winters',
+ 'Wise',
+ 'Witt',
+ 'Wolf',
+ 'Wolfe',
+ 'Wong',
+ 'Wood',
+ 'Woodard',
+ 'Woods',
+ 'Woodward',
+ 'Wooten',
+ 'Workman',
+ 'Wright',
+ 'Wyatt',
+ 'Wynn',
+ 'Yang',
+ 'Yates',
+ 'York',
+ 'Young',
+ 'Zamora',
+ 'Zimmerman'
+];
diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index 6c033931e6b..fadf35e27cc 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -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,
diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts
index 01d58af7483..e36f6ac8fd0 100644
--- a/src/demo-app/demo-app/demo-app.ts
+++ b/src/demo-app/demo-app/demo-app.ts
@@ -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'},
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index b25605399e3..66a10286c9d 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -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},
diff --git a/src/lib/core/data-table/cell.ts b/src/lib/core/data-table/cell.ts
new file mode 100644
index 00000000000..b63ee504eea
--- /dev/null
+++ b/src/lib/core/data-table/cell.ts
@@ -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) { }
+}
+
+/**
+ * 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) { }
+}
+
+/**
+ * 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}`);
+ }
+}
diff --git a/src/lib/core/data-table/data-source.ts b/src/lib/core/data-table/data-source.ts
new file mode 100644
index 00000000000..8314d1299bd
--- /dev/null
+++ b/src/lib/core/data-table/data-source.ts
@@ -0,0 +1,9 @@
+import {Observable} from 'rxjs/Observable';
+
+export interface CollectionViewer {
+ viewChanged: Observable<{start: number, end: number}>;
+}
+
+export abstract class DataSource {
+ abstract connect(collectionViewer: CollectionViewer): Observable;
+}
diff --git a/src/lib/core/data-table/data-table.spec.ts b/src/lib/core/data-table/data-table.spec.ts
new file mode 100644
index 00000000000..8dd81f57cd9
--- /dev/null
+++ b/src/lib/core/data-table/data-table.spec.ts
@@ -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;
+
+ 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 {
+ isConnected = false;
+
+ _dataChange = new BehaviorSubject([]);
+ 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 {
+ 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: `
+
+
+ Column A
+ {{row.a}}
+
+
+
+ Column B
+ {{row.b}}
+
+
+
+ Column C
+ {{row.c}}
+
+
+
+
+
+ `
+})
+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'
+ };
+ }
+ };
+ }
+};
diff --git a/src/lib/core/data-table/data-table.ts b/src/lib/core/data-table/data-table.ts
new file mode 100644
index 00000000000..922b49cda12
--- /dev/null
+++ b/src/lib/core/data-table/data-table.ts
@@ -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: `
+
+
+ `,
+ 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;
+
+ // 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();
+
+ // 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;
+
+ /** Template used as the header container. */
+ @ContentChild(CdkHeaderRowDef) _headerDefinition: CdkHeaderRowDef;
+
+ /** Set of templates that used as the data row containers. */
+ @ContentChildren(CdkRowDef) _rowDefinitions: QueryList;
+
+ 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;
+ });
+ }
+}
diff --git a/src/lib/core/data-table/index.ts b/src/lib/core/data-table/index.ts
new file mode 100644
index 00000000000..03d8d5491a6
--- /dev/null
+++ b/src/lib/core/data-table/index.ts
@@ -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 { }
diff --git a/src/lib/core/data-table/row.ts b/src/lib/core/data-table/row.ts
new file mode 100644
index 00000000000..79baa7bfb21
--- /dev/null
+++ b/src/lib/core/data-table/row.ts
@@ -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) { }
+}
+
+/**
+ * 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) { }
+}
+
+/**
+ * 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: '',
+ 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: '',
+ host: {
+ 'class': 'cdk-row',
+ 'role': 'row',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CdkRow { }
diff --git a/src/lib/core/testing/jasmine-matchers.ts b/src/lib/core/testing/jasmine-matchers.ts
new file mode 100644
index 00000000000..037cfcf7a5d
--- /dev/null
+++ b/src/lib/core/testing/jasmine-matchers.ts
@@ -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;
+ }
+ };
+ }
+};
diff --git a/src/lib/module.ts b/src/lib/module.ts
index fa603ea37c1..255ba9b5d20 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -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 */
diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts
index ac3d936034b..7eeeb82012d 100644
--- a/src/lib/public_api.ts
+++ b/src/lib/public_api.ts
@@ -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';
diff --git a/src/lib/typings.d.ts b/src/lib/typings.d.ts
index ce4ae9b66cf..316a7f5b4ec 100644
--- a/src/lib/typings.d.ts
+++ b/src/lib/typings.d.ts
@@ -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;
+ }
+}
diff --git a/tools/gulp/packaging/rollup-helpers.ts b/tools/gulp/packaging/rollup-helpers.ts
index aff500a276a..784724ac48f 100644
--- a/tools/gulp/packaging/rollup-helpers.ts
+++ b/tools/gulp/packaging/rollup-helpers.ts
@@ -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 = {