Skip to content

Commit e7533a5

Browse files
amcdnljelbourn
authored andcommitted
feat(schematics): table schematic (#10012)
1 parent 279c112 commit e7533a5

11 files changed

+416
-0
lines changed

schematics/collection.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
{
33
"$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
44
"schematics": {
5+
// Creates a table component
6+
"materialTable": {
7+
"description": "Create a table component",
8+
"factory": "./table/index",
9+
"schema": "./table/schema.json",
10+
"aliases": [ "material-table" ]
11+
},
512
// Creates toolbar and navigation components
613
"materialNav": {
714
"description": "Create a responsive navigation component",

schematics/table/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Table
2+
Creates a table component with sorting and paginator.
3+
4+
Command: `ng generate material-table --collection=material-schematics`
5+
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { merge } from 'rxjs/observable/merge';
2+
import { of as observableOf } from 'rxjs/observable/of';
3+
import { DataSource } from '@angular/cdk/collections';
4+
import { MatPaginator, MatSort } from '@angular/material';
5+
import { map } from 'rxjs/operators/map';
6+
import { Observable } from 'rxjs/Observable';
7+
8+
// TODO: Replace this with your own data model type
9+
export interface <%= classify(name) %>Item {
10+
name: string;
11+
id: number;
12+
}
13+
14+
// TODO: replace this with real data from your application
15+
const EXAMPLE_DATA = <%= classify(name) %>Item[] = [
16+
{id: 1, name: 'Hydrogen'},
17+
{id: 2, name: 'Helium'},
18+
{id: 3, name: 'Lithium'},
19+
{id: 4, name: 'Beryllium'},
20+
{id: 5, name: 'Boron'},
21+
{id: 6, name: 'Carbon'},
22+
{id: 7, name: 'Nitrogen'},
23+
{id: 8, name: 'Oxygen'},
24+
{id: 9, name: 'Fluorine'},
25+
{id: 10, name: 'Neon'},
26+
{id: 11, name: 'Sodium'},
27+
{id: 12, name: 'Magnesium'},
28+
{id: 13, name: 'Aluminum'},
29+
{id: 14, name: 'Silicon'},
30+
{id: 15, name: 'Phosphorus'},
31+
{id: 16, name: 'Sulfur'},
32+
{id: 17, name: 'Chlorine'},
33+
{id: 18, name: 'Argon'},
34+
{id: 19, name: 'Potassium'},
35+
{id: 20, name: 'Calcium'},
36+
];
37+
38+
/**
39+
* Data source for the <%= classify(name) %> view. This class should
40+
* encapsulate all logic for fetching and manipulating the displayed data
41+
* (including sorting, pagination, and filtering).
42+
*/
43+
export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> {
44+
data: <%= classify(name) %>Item[] = EXAMPLE_DATA;
45+
46+
constructor(private paginator: MatPaginator, private sort: MatSort) {
47+
super();
48+
}
49+
50+
/**
51+
* Connect this data source to the table. The table will only update when
52+
* the returned stream emits new items.
53+
* @returns A stream of the items to be rendered.
54+
*/
55+
connect(): Observable<<%= classify(name) %>Item[]> {
56+
// Combine everything that affects the rendered data into one update
57+
// stream for the data-table to consume.
58+
const dataMutations = [
59+
observableOf(this.data),
60+
this.paginator.page,
61+
this.sort.sortChange
62+
];
63+
64+
return merge(...dataMutations).pipe(map(() => {
65+
return this.getPagedData(this.getSortedData(this.data));
66+
}));
67+
}
68+
69+
/**
70+
* Called when the table is being destroyed. Use this function, to clean up
71+
* any open connections or free any held resources that were set up during connect.
72+
*/
73+
disconnect() {}
74+
75+
/**
76+
* Paginate the data (client-side). If you're using server-side pagination,
77+
* this would be replaced by requesting the appropriate data from the server.
78+
*/
79+
private getPagedData(data: <%= classify(name) %>Item[]) {
80+
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
81+
return data.splice(startIndex, this.paginator.pageSize);
82+
}
83+
84+
/**
85+
* Sort the data (client-side). If you're using server-side sorting,
86+
* this would be replaced by requesting the appropriate data from the server.
87+
*/
88+
private getSortedData(data: <%= classify(name) %>Item[]) {
89+
if (!this.sort.active || this.sort.direction === '') {
90+
return data;
91+
}
92+
93+
return data.sort((a, b) => {
94+
const isAsc = this.sort.direction == 'asc';
95+
switch (this.sort.active) {
96+
case 'name': return compare(a.name, b.name, isAsc);
97+
case 'id': return compare(+a.id, +b.id, isAsc);
98+
default: return 0;
99+
}
100+
});
101+
}
102+
}
103+
104+
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
105+
function compare(a, b, isAsc) {
106+
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
107+
}

schematics/table/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<div class="mat-elevation-z8">
2+
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
3+
4+
<!-- Id Column -->
5+
<ng-container matColumnDef="id">
6+
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
7+
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell>
8+
</ng-container>
9+
10+
<!-- Name Column -->
11+
<ng-container matColumnDef="name">
12+
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
13+
<mat-cell *matCellDef="let row">{{row.name}}</mat-cell>
14+
</ng-container>
15+
16+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
17+
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
18+
</mat-table>
19+
20+
<mat-paginator #paginator
21+
[length]="dataSource.data.length"
22+
[pageIndex]="0"
23+
[pageSize]="50"
24+
[pageSizeOptions]="[25, 50, 100, 250]">
25+
</mat-paginator>
26+
</div>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
import { fakeAsync, ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
5+
6+
describe('<%= classify(name) %>Component', () => {
7+
let component: <%= classify(name) %>Component;
8+
let fixture: ComponentFixture<<%= classify(name) %>Component>;
9+
10+
beforeEach(fakeAsync(() => {
11+
TestBed.configureTestingModule({
12+
declarations: [ <%= classify(name) %>Component ]
13+
})
14+
.compileComponents();
15+
16+
fixture = TestBed.createComponent(<%= classify(name) %>Component);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should compile', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Component, OnInit, ViewChild, <% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
2+
import { Observable } from 'rxjs/Observable';
3+
import { MatPaginator, MatSort } from '@angular/material';
4+
import { <%= classify(name) %>DataSource } from '<%= dasherize(name) %>-datasource';
5+
6+
@Component({
7+
selector: '<%= selector %>',<% if(inlineTemplate) { %>
8+
template: `
9+
<div class="mat-elevation-z8">
10+
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
11+
12+
<!-- Id Column -->
13+
<ng-container matColumnDef="id">
14+
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
15+
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell>
16+
</ng-container>
17+
18+
<!-- Name Column -->
19+
<ng-container matColumnDef="name">
20+
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
21+
<mat-cell *matCellDef="let row">{{row.name}}</mat-cell>
22+
</ng-container>
23+
24+
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
25+
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
26+
</mat-table>
27+
28+
<mat-paginator #paginator
29+
[length]="dataSource.data.length"
30+
[pageIndex]="0"
31+
[pageSize]="10"
32+
[pageSizeOptions]="[25, 50, 100, 250]">
33+
</mat-paginator>
34+
</div>
35+
`,<% } else { %>
36+
templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %>
37+
styles: []<% } else { %>
38+
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
39+
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
40+
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
41+
})
42+
export class <%= classify(name) %>Component implements OnInit {
43+
@ViewChild(MatPaginator) paginator: MatPaginator;
44+
@ViewChild(MatSort) sort: MatSort;
45+
dataSource: <%= classify(name) %>DataSource;
46+
47+
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
48+
displayedColumns = ['id', 'name'];
49+
50+
ngOnInit() {
51+
this.dataSource = new <%= classify(name) %>DataSource(this.paginator, this.sort);
52+
}
53+
}

schematics/table/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {chain, Rule, noop, Tree, SchematicContext} from '@angular-devkit/schematics';
2+
import {Schema} from './schema';
3+
import {addModuleImportToModule} from '../utils/ast';
4+
import {findModuleFromOptions} from '../utils/devkit-utils/find-module';
5+
import {buildComponent} from '../utils/devkit-utils/component';
6+
7+
/**
8+
* Scaffolds a new table component.
9+
* Internally it bootstraps the base component schematic
10+
*/
11+
export default function(options: Schema): Rule {
12+
return chain([
13+
buildComponent({...options}),
14+
options.skipImport ? noop() : addTableModulesToModule(options)
15+
]);
16+
}
17+
18+
/**
19+
* Adds the required modules to the relative module.
20+
*/
21+
function addTableModulesToModule(options: Schema) {
22+
return (host: Tree) => {
23+
const modulePath = findModuleFromOptions(host, options);
24+
addModuleImportToModule(host, modulePath, 'MatTableModule', '@angular/material');
25+
addModuleImportToModule(host, modulePath, 'MatPaginatorModule', '@angular/material');
26+
addModuleImportToModule(host, modulePath, 'MatSortModule', '@angular/material');
27+
return host;
28+
};
29+
}

schematics/table/index_spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
2+
import {join} from 'path';
3+
import {Tree} from '@angular-devkit/schematics';
4+
import {createTestApp} from '../utils/testing';
5+
import {getFileContent} from '@schematics/angular/utility/test';
6+
7+
const collectionPath = join(__dirname, '../collection.json');
8+
9+
describe('material-table-schematic', () => {
10+
let runner: SchematicTestRunner;
11+
const options = {
12+
name: 'foo',
13+
path: 'app',
14+
sourceDir: 'src',
15+
inlineStyle: false,
16+
inlineTemplate: false,
17+
changeDetection: 'Default',
18+
styleext: 'css',
19+
spec: true,
20+
module: undefined,
21+
export: false,
22+
prefix: undefined,
23+
viewEncapsulation: undefined,
24+
};
25+
26+
beforeEach(() => {
27+
runner = new SchematicTestRunner('schematics', collectionPath);
28+
});
29+
30+
it('should create table files and add them to module', () => {
31+
const tree = runner.runSchematic('materialTable', { ...options }, createTestApp());
32+
const files = tree.files;
33+
34+
expect(files).toContain('/src/app/foo/foo.component.css');
35+
expect(files).toContain('/src/app/foo/foo.component.html');
36+
expect(files).toContain('/src/app/foo/foo.component.spec.ts');
37+
expect(files).toContain('/src/app/foo/foo.component.ts');
38+
expect(files).toContain('/src/app/foo/foo-datasource.ts');
39+
40+
const moduleContent = getFileContent(tree, '/src/app/app.module.ts');
41+
expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.component'/);
42+
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooComponent\r?\n/m);
43+
44+
const datasourceContent = getFileContent(tree, '/src/app/foo/foo-datasource.ts');
45+
expect(datasourceContent).toContain('FooItem');
46+
expect(datasourceContent).toContain('FooDataSource');
47+
48+
const componentContent = getFileContent(tree, '/src/app/foo/foo.component.ts');
49+
expect(componentContent).toContain('FooDataSource');
50+
});
51+
52+
it('should add table imports to module', () => {
53+
const tree = runner.runSchematic('materialTable', { ...options }, createTestApp());
54+
const moduleContent = getFileContent(tree, '/src/app/app.module.ts');
55+
56+
expect(moduleContent).toContain('MatTableModule');
57+
expect(moduleContent).toContain('MatPaginatorModule');
58+
expect(moduleContent).toContain('MatSortModule');
59+
60+
expect(moduleContent).toContain(
61+
`import { MatTableModule, MatPaginatorModule, MatSortModule } from '@angular/material';`);
62+
});
63+
64+
});

schematics/table/schema.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {Schema as ComponentSchema} from '@schematics/angular/component/schema';
2+
3+
export interface Schema extends ComponentSchema {}

0 commit comments

Comments
 (0)