Skip to content

Commit ddc3aea

Browse files
committed
feat(schematics): add custom component schematic
1 parent 48495ec commit ddc3aea

10 files changed

+244
-3
lines changed

collection.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
},
1313
"component": {
1414
"aliases": ["c"],
15-
"extends": "@schematics/angular:component"
15+
"factory": "./schematics/component",
16+
"description": "Create an Angular component.",
17+
"schema": "./schematics/component/schema.json"
1618
},
1719
"directive": {
1820
"aliases": ["d"],

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
"devDependencies": {
3838
"@angular-devkit/architect": "^0.12.3",
3939
"@angular-devkit/build-angular": "^0.12.3",
40-
"@angular-devkit/core": "^7.1.3",
41-
"@angular-devkit/schematics": "^7.1.3",
40+
"@angular-devkit/core": "~7.2.0",
41+
"@angular-devkit/schematics": "~7.2.0",
4242
"@semantic-release/changelog": "^3.0.0",
4343
"@semantic-release/git": "^7.0.4",
4444
"@semantic-release/github": "^5.0.6",

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

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<p>
2+
<%= dasherize(name) %> works!
3+
</p>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { <%= classify(name) %>Page } from './<%= dasherize(name) %>.page';
5+
6+
describe('<%= classify(name) %>Page', () => {
7+
let component: <%= classify(name) %>Page;
8+
let fixture: ComponentFixture<<%= classify(name) %>Page>;
9+
10+
beforeEach(async(() => {
11+
TestBed.configureTestingModule({
12+
declarations: [ <%= classify(name) %>Page ],
13+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
14+
})
15+
.compileComponents();
16+
}));
17+
18+
beforeEach(() => {
19+
fixture = TestBed.createComponent(<%= classify(name) %>Page);
20+
component = fixture.componentInstance;
21+
fixture.detectChanges();
22+
});
23+
24+
it('should create', () => {
25+
expect(component).toBeTruthy();
26+
});
27+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component, OnInit } from '@angular/core';
2+
3+
@Component({
4+
selector: '<%= selector %>',
5+
templateUrl: './<%= dasherize(name) %>.component.html',
6+
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>'],
7+
})
8+
export class <%= classify(name) %>Component implements OnInit {
9+
10+
constructor() { }
11+
12+
ngOnInit() {}
13+
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { NgModule } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
5+
import { IonicModule } from '@ionic/angular';
6+
7+
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
8+
9+
@NgModule({
10+
imports: [ CommonModule, FormsModule,IonicModule,],
11+
declarations: [<%= classify(name) %>Component],
12+
exports: [<%= classify(name) %>Component]
13+
})
14+
export class <%= classify(name) %>ComponentModule {}

schematics/component/index.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { strings } from '@angular-devkit/core';
2+
import { Rule, SchematicsException, Tree, apply, branchAndMerge, chain, filter, mergeWith, move, noop, template, url } from '@angular-devkit/schematics';
3+
import { addSymbolToNgModuleMetadata } from '@schematics/angular/utility/ast-utils';
4+
import { InsertChange } from '@schematics/angular/utility/change';
5+
import { buildRelativePath } from '@schematics/angular/utility/find-module';
6+
import { parseName } from '@schematics/angular/utility/parse-name';
7+
import { buildDefaultPath, getProject } from '@schematics/angular/utility/project';
8+
import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation';
9+
import * as ts from 'typescript';
10+
11+
import { Schema as ComponentOptions } from './schema';
12+
13+
function buildSelector(options: ComponentOptions, projectPrefix: string) {
14+
let selector = strings.dasherize(options.name);
15+
16+
if (options.prefix) {
17+
selector = `${options.prefix}-${selector}`;
18+
} else if (options.prefix === undefined && projectPrefix) {
19+
selector = `${projectPrefix}-${selector}`;
20+
}
21+
22+
return selector;
23+
}
24+
25+
function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile {
26+
const text = host.read(modulePath);
27+
if (text === null) {
28+
throw new SchematicsException(`File ${modulePath} does not exist.`);
29+
}
30+
const sourceText = text.toString('utf-8');
31+
32+
return ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true);
33+
}
34+
35+
function addImportToNgModule(options: ComponentOptions): Rule {
36+
return (host: Tree) => {
37+
if (!options.module) {
38+
return host;
39+
}
40+
41+
const modulePath = options.module;
42+
const moduleSource = readIntoSourceFile(host, modulePath);
43+
44+
const componentModulePath = `/${options.path}/`
45+
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
46+
+ strings.dasherize(options.name)
47+
+ '.module';
48+
49+
const relativePath = buildRelativePath(modulePath, componentModulePath);
50+
const classifiedName = strings.classify(`${options.name}ComponentModule`);
51+
const importChanges = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', classifiedName, relativePath);
52+
53+
const importRecorder = host.beginUpdate(modulePath);
54+
for (const change of importChanges) {
55+
if (change instanceof InsertChange) {
56+
importRecorder.insertLeft(change.pos, change.toAdd);
57+
}
58+
}
59+
host.commitUpdate(importRecorder);
60+
return host;
61+
};
62+
}
63+
64+
export default function(options: ComponentOptions): Rule {
65+
return (host, context) => {
66+
if (!options.project) {
67+
throw new SchematicsException('Option (project) is required.');
68+
}
69+
70+
const project = getProject(host, options.project);
71+
72+
if (options.path === undefined) {
73+
options.path = buildDefaultPath(project);
74+
}
75+
76+
const parsedPath = parseName(options.path, options.name);
77+
options.name = parsedPath.name;
78+
options.path = parsedPath.path;
79+
options.selector = options.selector ? options.selector : buildSelector(options, project.prefix);
80+
81+
validateName(options.name);
82+
validateHtmlSelector(options.selector);
83+
84+
const templateSource = apply(url('./files'), [
85+
options.spec ? noop() : filter(p => !p.endsWith('.spec.ts')),
86+
template({
87+
...strings,
88+
'if-flat': (s: string) => options.flat ? '' : s,
89+
...options,
90+
}),
91+
move(parsedPath.path),
92+
]);
93+
94+
return chain([
95+
branchAndMerge(chain([
96+
addImportToNgModule(options),
97+
mergeWith(templateSource),
98+
])),
99+
])(host, context);
100+
};
101+
}

schematics/component/schema.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface Schema {
2+
path?: string;
3+
project?: string;
4+
name: string;
5+
prefix?: string;
6+
styleext?: string;
7+
spec?: boolean;
8+
flat?: boolean;
9+
selector?: string;
10+
module?: string;
11+
}

schematics/component/schema.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsIonicAngularComponent",
4+
"title": "@ionic/angular Component Options Schema",
5+
"type": "object",
6+
"properties": {
7+
"path": {
8+
"type": "string",
9+
"format": "path",
10+
"description": "The path to create the page",
11+
"visible": false
12+
},
13+
"project": {
14+
"type": "string",
15+
"description": "The name of the project",
16+
"$default": {
17+
"$source": "projectName"
18+
}
19+
},
20+
"name": {
21+
"type": "string",
22+
"description": "The name of the page",
23+
"$default": {
24+
"$source": "argv",
25+
"index": 0
26+
}
27+
},
28+
"prefix": {
29+
"type": "string",
30+
"description": "The prefix to apply to generated selectors",
31+
"alias": "p",
32+
"oneOf": [
33+
{
34+
"maxLength": 0
35+
},
36+
{
37+
"minLength": 1,
38+
"format": "html-selector"
39+
}
40+
]
41+
},
42+
"styleext": {
43+
"type": "string",
44+
"description": "The file extension of the style file for the page",
45+
"default": "css"
46+
},
47+
"spec": {
48+
"type": "boolean",
49+
"description": "Specifies if a spec file is generated",
50+
"default": true
51+
},
52+
"flat": {
53+
"type": "boolean",
54+
"description": "Flag to indicate if a dir is created",
55+
"default": false
56+
},
57+
"selector": {
58+
"type": "string",
59+
"format": "html-selector",
60+
"description": "The selector to use for the page"
61+
},
62+
"module": {
63+
"type": "string",
64+
"description": "Allows specification of the declaring module",
65+
"alias": "m"
66+
}
67+
},
68+
"required": []
69+
}

0 commit comments

Comments
 (0)