-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(component): add custom component schematic (#68)
This feature better aligns lazy-loaded page structure (a default of Ionic 4) with generated components. Previously, generating a component would register the component with the root module, which would make it unavailable in all pages that were lazy-loaded. The new component schematic offers a new option: `--createModule`. It can be combined with other options such as `--module` and `--export` to handle several use cases. * test1: Generates a component and does not register it. * test2: Generates a component with an NgModule, but does not register it. * test3: Generates a component with an NgModule and registers the module with tab1's NgModule. * test4: Generates a component and registers the component with tab1's NgModule. * test5: Generates a component, registers the component with a component NgModule, and exports it from that NgModule. ``` ng g component test1 ng g component test2 --createModule ng g component test3 --createModule --module /src/app/tab1/tab1.module.ts ng g component test4 --entryComponent --module /src/app/tab1/tab1.module.ts ng g component test5 --export --module /src/app/components.module.ts ```
- Loading branch information
1 parent
48495ec
commit 527f54e
Showing
13 changed files
with
344 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
3 changes: 3 additions & 0 deletions
3
schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<p> | ||
<%= dasherize(name) %> works! | ||
</p> |
27 changes: 27 additions & 0 deletions
27
schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; | ||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
||
import { <%= classify(name) %>Page } from './<%= dasherize(name) %>.page'; | ||
|
||
describe('<%= classify(name) %>Page', () => { | ||
let component: <%= classify(name) %>Page; | ||
let fixture: ComponentFixture<<%= classify(name) %>Page>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
declarations: [ <%= classify(name) %>Page ], | ||
schemas: [CUSTOM_ELEMENTS_SCHEMA], | ||
}) | ||
.compileComponents(); | ||
})); | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(<%= classify(name) %>Page); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should create', () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
14 changes: 14 additions & 0 deletions
14
schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Component, OnInit } from '@angular/core'; | ||
|
||
@Component({ | ||
selector: '<%= selector %>', | ||
templateUrl: './<%= dasherize(name) %>.component.html', | ||
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>'], | ||
}) | ||
export class <%= classify(name) %>Component implements OnInit { | ||
|
||
constructor() { } | ||
|
||
ngOnInit() {} | ||
|
||
} |
14 changes: 14 additions & 0 deletions
14
schematics/component/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
import { FormsModule } from '@angular/forms'; | ||
|
||
import { IonicModule } from '@ionic/angular'; | ||
|
||
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component'; | ||
|
||
@NgModule({ | ||
imports: [ CommonModule, FormsModule,IonicModule,], | ||
declarations: [<%= classify(name) %>Component], | ||
exports: [<%= classify(name) %>Component] | ||
}) | ||
export class <%= classify(name) %>ComponentModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { strings } from '@angular-devkit/core'; | ||
import { Rule, SchematicsException, Tree, apply, branchAndMerge, chain, filter, mergeWith, move, noop, template, url } from '@angular-devkit/schematics'; | ||
import { addDeclarationToModule, addEntryComponentToModule, addExportToModule, addSymbolToNgModuleMetadata } from '@schematics/angular/utility/ast-utils'; | ||
import { InsertChange } from '@schematics/angular/utility/change'; | ||
import { buildRelativePath } from '@schematics/angular/utility/find-module'; | ||
import { parseName } from '@schematics/angular/utility/parse-name'; | ||
import { buildDefaultPath, getProject } from '@schematics/angular/utility/project'; | ||
import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; | ||
import * as ts from 'typescript'; | ||
|
||
import { buildSelector } from '../util'; | ||
|
||
import { Schema as ComponentOptions } from './schema'; | ||
|
||
function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile { | ||
const text = host.read(modulePath); | ||
if (text === null) { | ||
throw new SchematicsException(`File ${modulePath} does not exist.`); | ||
} | ||
const sourceText = text.toString('utf-8'); | ||
|
||
return ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); | ||
} | ||
|
||
function addImportToNgModule(options: ComponentOptions): Rule { | ||
return (host: Tree) => { | ||
if (!options.module) { | ||
return host; | ||
} | ||
if (!options.createModule && options.module) { | ||
addImportToDeclarations(host, options); | ||
} | ||
if (options.createModule && options.module) { | ||
addImportToImports(host, options); | ||
} | ||
return host; | ||
}; | ||
} | ||
|
||
function addImportToDeclarations(host: Tree, options: ComponentOptions): void { | ||
if (options.module) { | ||
const modulePath = options.module; | ||
let source = readIntoSourceFile(host, modulePath); | ||
|
||
const componentPath = `/${options.path}/` | ||
+ (options.flat ? '' : strings.dasherize(options.name) + '/') | ||
+ strings.dasherize(options.name) | ||
+ '.component'; | ||
const relativePath = buildRelativePath(modulePath, componentPath); | ||
const classifiedName = strings.classify(`${options.name}Component`); | ||
const declarationChanges = addDeclarationToModule(source, | ||
modulePath, | ||
classifiedName, | ||
relativePath); | ||
|
||
const declarationRecorder = host.beginUpdate(modulePath); | ||
for (const change of declarationChanges) { | ||
if (change instanceof InsertChange) { | ||
declarationRecorder.insertLeft(change.pos, change.toAdd); | ||
} | ||
} | ||
host.commitUpdate(declarationRecorder); | ||
|
||
if (options.export) { | ||
// Need to refresh the AST because we overwrote the file in the host. | ||
source = readIntoSourceFile(host, modulePath); | ||
|
||
const exportRecorder = host.beginUpdate(modulePath); | ||
const exportChanges = addExportToModule(source, modulePath, | ||
strings.classify(`${options.name}Component`), | ||
relativePath); | ||
|
||
for (const change of exportChanges) { | ||
if (change instanceof InsertChange) { | ||
exportRecorder.insertLeft(change.pos, change.toAdd); | ||
} | ||
} | ||
host.commitUpdate(exportRecorder); | ||
} | ||
|
||
if (options.entryComponent) { | ||
// Need to refresh the AST because we overwrote the file in the host. | ||
source = readIntoSourceFile(host, modulePath); | ||
|
||
const entryComponentRecorder = host.beginUpdate(modulePath); | ||
const entryComponentChanges = addEntryComponentToModule( | ||
source, modulePath, | ||
strings.classify(`${options.name}Component`), | ||
relativePath); | ||
|
||
for (const change of entryComponentChanges) { | ||
if (change instanceof InsertChange) { | ||
entryComponentRecorder.insertLeft(change.pos, change.toAdd); | ||
} | ||
} | ||
host.commitUpdate(entryComponentRecorder); | ||
} | ||
} | ||
} | ||
|
||
function addImportToImports(host: Tree, options: ComponentOptions): void { | ||
if (options.module) { | ||
const modulePath = options.module; | ||
const moduleSource = readIntoSourceFile(host, modulePath); | ||
|
||
const componentModulePath = `/${options.path}/` | ||
+ (options.flat ? '' : strings.dasherize(options.name) + '/') | ||
+ strings.dasherize(options.name) | ||
+ '.module'; | ||
|
||
const relativePath = buildRelativePath(modulePath, componentModulePath); | ||
const classifiedName = strings.classify(`${options.name}ComponentModule`); | ||
const importChanges = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', classifiedName, relativePath); | ||
|
||
const importRecorder = host.beginUpdate(modulePath); | ||
for (const change of importChanges) { | ||
if (change instanceof InsertChange) { | ||
importRecorder.insertLeft(change.pos, change.toAdd); | ||
} | ||
} | ||
host.commitUpdate(importRecorder); | ||
} | ||
} | ||
|
||
export default function(options: ComponentOptions): Rule { | ||
return (host, context) => { | ||
if (!options.project) { | ||
throw new SchematicsException('Option (project) is required.'); | ||
} | ||
|
||
const project = getProject(host, options.project); | ||
|
||
if (options.path === undefined) { | ||
options.path = buildDefaultPath(project); | ||
} | ||
|
||
const parsedPath = parseName(options.path, options.name); | ||
options.name = parsedPath.name; | ||
options.path = parsedPath.path; | ||
options.selector = options.selector ? options.selector : buildSelector(options, project.prefix); | ||
|
||
validateName(options.name); | ||
validateHtmlSelector(options.selector); | ||
|
||
const templateSource = apply(url('./files'), [ | ||
options.spec ? noop() : filter(p => !p.endsWith('.spec.ts')), | ||
options.createModule ? noop() : filter(p => !p.endsWith('.module.ts')), | ||
template({ | ||
...strings, | ||
'if-flat': (s: string) => options.flat ? '' : s, | ||
...options, | ||
}), | ||
move(parsedPath.path), | ||
]); | ||
|
||
return chain([ | ||
branchAndMerge(chain([ | ||
addImportToNgModule(options), | ||
mergeWith(templateSource), | ||
])), | ||
])(host, context); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export interface Schema { | ||
path?: string; | ||
project?: string; | ||
name: string; | ||
prefix?: string; | ||
styleext?: string; | ||
spec?: boolean; | ||
flat?: boolean; | ||
selector?: string; | ||
createModule?: boolean; | ||
module?: string; | ||
export?: boolean; | ||
entryComponent?: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
{ | ||
"$schema": "http://json-schema.org/schema", | ||
"id": "SchematicsIonicAngularComponent", | ||
"title": "@ionic/angular Component Options Schema", | ||
"type": "object", | ||
"properties": { | ||
"path": { | ||
"type": "string", | ||
"format": "path", | ||
"description": "The path to create the page", | ||
"visible": false | ||
}, | ||
"project": { | ||
"type": "string", | ||
"description": "The name of the project", | ||
"$default": { | ||
"$source": "projectName" | ||
} | ||
}, | ||
"name": { | ||
"type": "string", | ||
"description": "The name of the page", | ||
"$default": { | ||
"$source": "argv", | ||
"index": 0 | ||
} | ||
}, | ||
"prefix": { | ||
"type": "string", | ||
"description": "The prefix to apply to generated selectors", | ||
"alias": "p", | ||
"oneOf": [ | ||
{ | ||
"maxLength": 0 | ||
}, | ||
{ | ||
"minLength": 1, | ||
"format": "html-selector" | ||
} | ||
] | ||
}, | ||
"styleext": { | ||
"type": "string", | ||
"description": "The file extension of the style file for the page", | ||
"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", | ||
"default": false | ||
}, | ||
"selector": { | ||
"type": "string", | ||
"format": "html-selector", | ||
"description": "The selector to use for the page" | ||
}, | ||
"createModule": { | ||
"type": "boolean", | ||
"description": "Allows creating an NgModule for the component", | ||
"default": false | ||
}, | ||
"module": { | ||
"type": "string", | ||
"description": "Allows adding to an NgModule's imports or declarations" | ||
}, | ||
"export": { | ||
"type": "boolean", | ||
"default": false, | ||
"description": "When true, the declaring NgModule exports this component." | ||
}, | ||
"entryComponent": { | ||
"type": "boolean", | ||
"default": false, | ||
"description": "When true, the new component is the entry component of the declaring NgModule." | ||
} | ||
}, | ||
"required": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { strings } from '@angular-devkit/core'; | ||
|
||
export function buildSelector(options: any, projectPrefix: string) { | ||
let selector = strings.dasherize(options.name); | ||
|
||
if (options.prefix) { | ||
selector = `${options.prefix}-${selector}`; | ||
} else if (options.prefix === undefined && projectPrefix) { | ||
selector = `${projectPrefix}-${selector}`; | ||
} | ||
|
||
return selector; | ||
} |
Oops, something went wrong.