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}}%
+
+
+
+
+
+ + + + 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 = {