Skip to content

feat(schematics): navigation schematic #10009

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 5 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 toolbar and navigation components
"materialNav": {
"description": "Create a responsive navigation component",
"factory": "./nav/index",
"schema": "./nav/schema.json",
"aliases": [ "material-nav" ]
},
// Adds Angular Material to an application without changing any templates
"materialShell": {
"description": "Create a Material shell",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.sidenav-container {
height: 100%;
}

.sidenav {
width: 200px;
box-shadow: 3px 0 6px rgba(0,0,0,.24);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<mat-sidenav-container class="sidenav-container">
<mat-sidenav
#drawer
class="sidenav"
fixedInViewport="true"
[attr.role]="isHandset ? 'dialog' : 'navigation'"
[mode]="isHandset ? 'over' : 'side'"
[opened]="!(isHandset | async)!.matches">
<mat-toolbar color="primary">Menu</mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary">
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()"
*ngIf="(isHandset | async)!.matches">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
<span>Application Title</span>
</mat-toolbar>
</mat-sidenav-content>
</mat-sidenav-container>
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,58 @@
import { Component, <% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
import { Observable } from 'rxjs/Observable';

@Component({
selector: '<%= selector %>',<% if(inlineTemplate) { %>
Copy link
Contributor

Choose a reason for hiding this comment

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

So I guess schematics doesn't have a way to insert contents of another file here? Do they have a way to add comments that will be stripped out of the generated file? If so it would be good to add some comments for people maintaining this file that they need to update in 2 places

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya, its very painful. I'm thinking about making a util to handle this automatically.

template: `
<mat-sidenav-container class="sidenav-container">
<mat-sidenav
#drawer
class="sidenav"
fixedInViewport="true"
[attr.role]="isHandset ? 'dialog' : 'navigation'"
[mode]="isHandset ? 'over' : 'side'"
[opened]="!(isHandset | async)!.matches">
<mat-toolbar color="primary">Menu</mat-toolbar>
<mat-nav-list>
<a mat-list-item href="#">Link 1</a>
<a mat-list-item href="#">Link 2</a>
<a mat-list-item href="#">Link 3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary">
<button
type="button"
aria-label="Toggle sidenav"
mat-icon-button
(click)="drawer.toggle()"
*ngIf="(isHandset | async)!.matches">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
<span>Application Title</span>
</mat-toolbar>
</mat-sidenav-content>
</mat-sidenav-container>
`,<% } else { %>
templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %>
styles: [
`
.sidenav-container {
height: 100%;
}

.sidenav {
width: 200px;
box-shadow: 3px 0 6px rgba(0,0,0,.24);
}
`
]<% } else { %>
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be if(viewEncapsulation !== 'Emulated')?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code was copied from the base create component schematic.

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 update the base schematic?

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 ^^

encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
})
export class <%= classify(name) %>Component {
isHandset: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.Handset);
constructor(private breakpointObserver: BreakpointObserver) {}
}
32 changes: 32 additions & 0 deletions schematics/nav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 navigation component.
* Internally it bootstraps the base component schematic
*/
export default function(options: Schema): Rule {
return chain([
buildComponent({ ...options }),
options.skipImport ? noop() : addNavModulesToModule(options)
]);
}

/**
* Adds the required modules to the relative module.
*/
function addNavModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
addModuleImportToModule(host, modulePath, 'LayoutModule', '@angular/cdk/layout');
addModuleImportToModule(host, modulePath, 'MatToolbarModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatSidenavModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatListModule', '@angular/material');
return host;
};
}
61 changes: 61 additions & 0 deletions schematics/nav/index_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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-nav-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 nav files and add them to module', () => {
const tree = runner.runSchematic('materialNav', { ...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');

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

it('should add nav imports to module', () => {
const tree = runner.runSchematic('materialNav', { ...options }, createTestApp());
const moduleContent = getFileContent(tree, '/src/app/app.module.ts');

expect(moduleContent).toContain('LayoutModule');
expect(moduleContent).toContain('MatToolbarModule');
expect(moduleContent).toContain('MatButtonModule');
expect(moduleContent).toContain('MatSidenavModule');
expect(moduleContent).toContain('MatIconModule');
expect(moduleContent).toContain('MatListModule');

expect(moduleContent).toContain(`import { LayoutModule } from '@angular/cdk/layout';`);
expect(moduleContent).toContain(
// tslint:disable-next-line
`import { MatToolbarModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule } from '@angular/material';`);
});

});
3 changes: 3 additions & 0 deletions schematics/nav/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 {}
99 changes: 99 additions & 0 deletions schematics/nav/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsMaterialNav",
"title": "Material Nav Options Schema",
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The path to create the component.",
"default": "app",
"visible": false
},
"sourceDir": {
"type": "string",
"description": "The path of the source directory.",
"default": "src",
"alias": "sd",
"visible": false
},
"appRoot": {
"type": "string",
"description": "The root of the application.",
"visible": false
},
"name": {
"type": "string",
"description": "The name of the component."
},
"inlineStyle": {
"description": "Specifies if the style will be in the ts file.",
"type": "boolean",
"default": false,
"alias": "is"
},
"inlineTemplate": {
"description": "Specifies if the template will be in the ts file.",
"type": "boolean",
"default": false,
"alias": "it"
},
"viewEncapsulation": {
"description": "Specifies the view encapsulation strategy.",
"enum": ["Emulated", "Native", "None"],
"type": "string",
Copy link
Contributor

Choose a reason for hiding this comment

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

"default": "Emulated"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code was copied from the base create component schematic.

Copy link
Member

Choose a reason for hiding this comment

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

Not really good enough reason on its own; Hans has mentioned that those base schematics are not meant to be normative.

"default": "Emulated",
"alias": "ve"
},
"changeDetection": {
"description": "Specifies the change detection strategy.",
"enum": ["Default", "OnPush"],
"type": "string",
"default": "Default",
"alias": "cd"
},
"prefix": {
"type": "string",
"description": "The prefix to apply to generated selectors.",
"default": "app",
"alias": "p"
},
"styleext": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css"
},
"spec": {
"type": "boolean",
"description": "Specifies if a spec file is generated.",
"default": true
},
"flat": {
"type": "boolean",
"description": "Flag to indicate if a dir is created.",
Copy link
Member

Choose a reason for hiding this comment

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

Unclear what "dir" it's referring to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code was copied from the base create component schematic.

"default": false
},
"skipImport": {
"type": "boolean",
"description": "Flag to skip the module import.",
"default": false
},
"selector": {
"type": "string",
"description": "The selector to use for the component."
},
"module": {
"type": "string",
"description": "Allows specification of the declaring module.",
"alias": "m"
},
"export": {
"type": "boolean",
"default": false,
"description": "Specifies if declaring module exports the component."
}
},
"required": [
"name"
]
}
5 changes: 4 additions & 1 deletion schematics/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
"jasmine",
"node"
]
}
},
"exclude": [
"*/files/**/*"
]
}