-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(schematics): table schematic #10012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
de5177d
4fe9892
13fbd00
4801ed9
42b2923
dbaf5a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Table | ||
Creates a table component with sorting and paginator. | ||
|
||
Command: `ng generate material-table --collection=material-schematics` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { merge } from 'rxjs/observable/merge'; | ||
import { of as observableOf } from 'rxjs/observable/of'; | ||
import { DataSource } from '@angular/cdk/collections'; | ||
import { MatPaginator, MatSort } from '@angular/material'; | ||
import { map } from 'rxjs/operators/map'; | ||
import { Observable } from 'rxjs/Observable'; | ||
|
||
/** TODO: Replace this with your own data model type */ | ||
export interface <%= classify(name) %>Item { | ||
name: string; | ||
id: number; | ||
} | ||
|
||
// TODO: replace this with real data from your application | ||
const EXAMPLE_DATA = <%= classify(name) %>Item[] = [ | ||
{id: 1, name: 'Hydrogen'}, | ||
{id: 2, name: 'Helium'}, | ||
{id: 3, name: 'Lithium'}, | ||
{id: 4, name: 'Beryllium'}, | ||
{id: 5, name: 'Boron'}, | ||
{id: 6, name: 'Carbon'}, | ||
{id: 7, name: 'Nitrogen'}, | ||
{id: 8, name: 'Oxygen'}, | ||
{id: 9, name: 'Fluorine'}, | ||
{id: 10, name: 'Neon'}, | ||
{id: 11, name: 'Sodium'}, | ||
{id: 12, name: 'Magnesium'}, | ||
{id: 13, name: 'Aluminum'}, | ||
{id: 14, name: 'Silicon'}, | ||
{id: 15, name: 'Phosphorus'}, | ||
{id: 16, name: 'Sulfur'}, | ||
{id: 17, name: 'Chlorine'}, | ||
{id: 18, name: 'Argon'}, | ||
{id: 19, name: 'Potassium'}, | ||
{id: 20, name: 'Calcium'}, | ||
]; | ||
|
||
export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a class description like
|
||
data: <%= classify(name) %>Item[] = EXAMPLE_DATA; | ||
|
||
constructor(private _paginator: MatPaginator, private _sort: MatSort) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though we use underscores for private variables in Material, I believe in this case we should follow the standard Angular-way:
|
||
super(); | ||
} | ||
|
||
/** | ||
* Connect this data source to the table. The table will only update when | ||
* the returned stream emits new items. | ||
* @returns A stream of the items to be rendered. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do other schematics use jsdoc format (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hansl ^^ |
||
*/ | ||
connect(): Observable<<%= classify(name) %>Item[]> { | ||
// Combine everything that affects the rendered data into one update | ||
// stream for the data-table to consume. | ||
const dataMutations = [ | ||
observableOf(this.data), | ||
this._paginator.page, | ||
this._sort.sortChange | ||
]; | ||
|
||
return merge(...dataMutations).pipe(map(() => { | ||
return this.getPagedData(this.getSortedData(this.data)); | ||
})); | ||
} | ||
|
||
disconnect() { | ||
// TODO: clean up any open connections, free any held resources, etc. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This TODO would make me believe I must do something here. Rather than TODO, maybe leave some jsdoc for the function.
|
||
} | ||
|
||
/** | ||
* Client-side page the data by slicing out the next from the data array. | ||
* If you are using external datasource for pagination, you would connect it here. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rephrase this as:
|
||
*/ | ||
private getPagedData(data: <%= classify(name) %>Item[]) { | ||
const startIndex = this._paginator.pageIndex * this._paginator.pageSize; | ||
return data.splice(startIndex, this._paginator.pageSize); | ||
} | ||
|
||
/** | ||
* Client-side sort the data array. | ||
* If you are using a external datasource for sorting, you would connect it here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rephrase this as:
|
||
*/ | ||
private getSortedData(data: <%= classify(name) %>Item[]) { | ||
if (!this._sort.active || this._sort.direction === '') { | ||
return data; | ||
} | ||
|
||
return data.sort((a, b) => { | ||
const isAsc = this._sort.direction == 'asc'; | ||
switch (this._sort.active) { | ||
case 'name': return compare(a.name, b.name, isAsc); | ||
case 'id': return compare(+a.id, +b.id, isAsc); | ||
default: return 0; | ||
} | ||
}); | ||
} | ||
} | ||
|
||
/** Simple sort comparator for example ID/Name columns. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /** Simple sort comparator for example ID/Name columns (for client-side sorting). */ |
||
function compare(a, b, isAsc) { | ||
return (a < b ? -1 : 1) * (isAsc ? 1 : -1); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<div class="mat-elevation-z8"> | ||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> | ||
|
||
<!-- Id Column --> | ||
<ng-container matColumnDef="id"> | ||
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell> | ||
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make it easier for users to know what can be renamed (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like |
||
</ng-container> | ||
|
||
<!-- Name Column --> | ||
<ng-container matColumnDef="name"> | ||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> | ||
<mat-cell *matCellDef="let row">{{row.name}}</mat-cell> | ||
</ng-container> | ||
|
||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> | ||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> | ||
</mat-table> | ||
|
||
<mat-paginator #paginator | ||
[length]="dataSource.data.length" | ||
[pageIndex]="0" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The length should come from the data, no? |
||
[pageSize]="50" | ||
[pageSizeOptions]="[25, 50, 100, 250]"> | ||
</mat-paginator> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
|
||
import { fakeAsync, ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component'; | ||
|
||
describe('<%= classify(name) %>Component', () => { | ||
let component: <%= classify(name) %>Component; | ||
let fixture: ComponentFixture<<%= classify(name) %>Component>; | ||
|
||
beforeEach(fakeAsync(() => { | ||
TestBed.configureTestingModule({ | ||
declarations: [ <%= classify(name) %>Component ] | ||
}) | ||
.compileComponents(); | ||
|
||
fixture = TestBed.createComponent(<%= classify(name) %>Component); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should compile', () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Component, OnInit, ViewChild, <% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core'; | ||
import { Observable } from 'rxjs/Observable'; | ||
import { MatPaginator, MatSort } from '@angular/material'; | ||
import { <%= classify(name) %>DataSource } from '<%= dasherize(name) %>-datasource'; | ||
|
||
@Component({ | ||
selector: '<%= selector %>',<% if(inlineTemplate) { %> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hansl are there any plans to make it easier to do templateUrl vs inline template without duplicating? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be done manually, ie. put the template content in a variable that’s used both here and in the separate file. Read that value from a file on disk. There’s no silver bullet for this one. |
||
template: ` | ||
<div class="mat-elevation-z8"> | ||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> | ||
|
||
<!-- Id Column --> | ||
<ng-container matColumnDef="id"> | ||
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell> | ||
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell> | ||
</ng-container> | ||
|
||
<!-- Name Column --> | ||
<ng-container matColumnDef="name"> | ||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> | ||
<mat-cell *matCellDef="let row">{{row.name}}</mat-cell> | ||
</ng-container> | ||
|
||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> | ||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> | ||
</mat-table> | ||
|
||
<mat-paginator #paginator | ||
[length]="dataSource.data.length" | ||
[pageIndex]="0" | ||
[pageSize]="10" | ||
[pageSizeOptions]="[25, 50, 100, 250]"> | ||
</mat-paginator> | ||
</div> | ||
`,<% } else { %> | ||
templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %> | ||
styles: []<% } else { %> | ||
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't write this, I copied the code from the default component template for this section. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't mean it shouldn't be corrected. |
||
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>, | ||
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %> | ||
}) | ||
export class <%= classify(name) %>Component implements OnInit { | ||
@ViewChild(MatPaginator) paginator: MatPaginator; | ||
@ViewChild(MatSort) sort: MatSort; | ||
dataSource: <%= classify(name) %>DataSource; | ||
|
||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ | ||
displayedColumns = ['id', 'name']; | ||
|
||
ngOnInit() { | ||
this.dataSource = new <%= classify(name) %>DataSource(this.paginator, this.sort); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {chain, Rule, noop, Tree, SchematicContext} from '@angular-devkit/schematics'; | ||
import {Schema} from './schema'; | ||
import {addModuleImportToModule} from '../utils/ast'; | ||
import {findModuleFromOptions} from '../utils/devkit-utils/find-module'; | ||
import {buildComponent} from '../utils/devkit-utils/component'; | ||
|
||
/** | ||
* Scaffolds a new table component. | ||
* Internally it bootstraps the base component schematic | ||
*/ | ||
export default function(options: Schema): Rule { | ||
return chain([ | ||
buildComponent({...options}), | ||
options.skipImport ? noop() : addTableModulesToModule(options) | ||
]); | ||
} | ||
|
||
/** | ||
* Adds the required modules to the relative module. | ||
*/ | ||
function addTableModulesToModule(options: Schema) { | ||
return (host: Tree) => { | ||
const modulePath = findModuleFromOptions(host, options); | ||
addModuleImportToModule(host, modulePath, 'MatTableModule', '@angular/material'); | ||
addModuleImportToModule(host, modulePath, 'MatPaginatorModule', '@angular/material'); | ||
addModuleImportToModule(host, modulePath, 'MatSortModule', '@angular/material'); | ||
return host; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; | ||
import {join} from 'path'; | ||
import {Tree} from '@angular-devkit/schematics'; | ||
import {createTestApp} from '../utils/testing'; | ||
import {getFileContent} from '@schematics/angular/utility/test'; | ||
|
||
const collectionPath = join(__dirname, '../collection.json'); | ||
|
||
describe('material-table-schematic', () => { | ||
let runner: SchematicTestRunner; | ||
const options = { | ||
name: 'foo', | ||
path: 'app', | ||
sourceDir: 'src', | ||
inlineStyle: false, | ||
inlineTemplate: false, | ||
changeDetection: 'Default', | ||
styleext: 'css', | ||
spec: true, | ||
module: undefined, | ||
export: false, | ||
prefix: undefined, | ||
viewEncapsulation: undefined, | ||
}; | ||
|
||
beforeEach(() => { | ||
runner = new SchematicTestRunner('schematics', collectionPath); | ||
}); | ||
|
||
it('should create table files and add them to module', () => { | ||
const tree = runner.runSchematic('materialTable', { ...options }, createTestApp()); | ||
const files = tree.files; | ||
|
||
expect(files).toContain('/src/app/foo/foo.component.css'); | ||
expect(files).toContain('/src/app/foo/foo.component.html'); | ||
expect(files).toContain('/src/app/foo/foo.component.spec.ts'); | ||
expect(files).toContain('/src/app/foo/foo.component.ts'); | ||
expect(files).toContain('/src/app/foo/foo-datasource.ts'); | ||
|
||
const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); | ||
expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.component'/); | ||
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooComponent\r?\n/m); | ||
|
||
const datasourceContent = getFileContent(tree, '/src/app/foo/foo-datasource.ts'); | ||
expect(datasourceContent).toContain('FooItem'); | ||
expect(datasourceContent).toContain('FooDataSource'); | ||
|
||
const componentContent = getFileContent(tree, '/src/app/foo/foo.component.ts'); | ||
expect(componentContent).toContain('FooDataSource'); | ||
}); | ||
|
||
it('should add table imports to module', () => { | ||
const tree = runner.runSchematic('materialTable', { ...options }, createTestApp()); | ||
const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); | ||
|
||
expect(moduleContent).toContain('MatTableModule'); | ||
expect(moduleContent).toContain('MatPaginatorModule'); | ||
expect(moduleContent).toContain('MatSortModule'); | ||
|
||
expect(moduleContent).toContain( | ||
`import { MatTableModule, MatPaginatorModule, MatSortModule } from '@angular/material';`); | ||
}); | ||
|
||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; | ||
|
||
export interface Schema extends ComponentSchema {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switch to
//
style comments to be consistent with other TODOs