Skip to content

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

Merged
merged 6 commits into from
Mar 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
{
"$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
// Creates a table component
"materialTable": {
"description": "Create a table component",
"factory": "./table/index",
"schema": "./table/schema.json",
"aliases": [ "material-table" ]
},
// Creates toolbar and navigation components
"materialNav": {
"description": "Create a responsive navigation component",
Expand Down
5 changes: 5 additions & 0 deletions schematics/table/README.md
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,107 @@
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'},
];

/**
* Data source for the <%= classify(name) %> view. This class should
* encapsulate all logic for fetching and manipulating the displayed data
* (including sorting, pagination, and filtering).
*/
export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> {
data: <%= classify(name) %>Item[] = EXAMPLE_DATA;

constructor(private paginator: MatPaginator, private sort: MatSort) {
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do other schematics use jsdoc format (e.g. @returns)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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));
}));
}

/**
* Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect.
*/
disconnect() {}

/**
* Paginate the data (client-side). If you're using server-side pagination,
* this would be replaced by requesting the appropriate data from the server.
*/
private getPagedData(data: <%= classify(name) %>Item[]) {
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}

/**
* Sort the data (client-side). If you're using server-side sorting,
* this would be replaced by requesting the appropriate data from the server.
*/
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 (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>
Copy link
Contributor

Choose a reason for hiding this comment

The 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. row)? Perhaps calling the data element instead of row?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like row better because its a bit more generic. First thing I'm gonna do when I download this is rename that, if its row I'm less likely to want to do this.

</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]="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) { %>
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) { %>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the !! necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
29 changes: 29 additions & 0 deletions schematics/table/index.ts
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;
};
}
64 changes: 64 additions & 0 deletions schematics/table/index_spec.ts
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';`);
});

});
3 changes: 3 additions & 0 deletions schematics/table/schema.d.ts
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 {}
Loading