In my last blog article, Ive shown how to leverage Schematics, the Angular CLIs code generator, to scaffold custom components. This article goes one step further and shows how to register generated building blocks like Components, Directives, Pipes, or Services with an existing NgModule
. For this Ill extend the example from the last article that generates a SideMenuComponent
. The source code shown here can also be found in my GitHub repository.
Schematics is currently experimental and can change in future.

Goal
To register the generated SideMenuComponent
we need to perform several tasks. For instance, we have to lookup the file with respective NgModule
. After this, we have to insert several lines into this file:
import { NgModule } from @angular/core;
import { CommonModule } from @angular/common;
// Add this line to reference component
import { SideMenuComponent } from ./side-menu/side-menu.component;
@NgModule({
imports: [
CommonModule
],
// Add this Line
declarations: [SideMenuComponent],
// Add this Line if we want to export the component too
exports: [SideMenuComponent]
})
export class CoreModule { }
As youve seen in the last listing, we have to create an import
statement at the beginning of the file. And then we have to add the imported component to the declarations
array and - if the caller requests it - to the exports
array too. If those arrays dont exist, we have to create them too.
The good message is, that the Angular CLI contains existing code for such tasks. Hence, we dont have to build everything from scratch. The next section shows some of those existing utility functions.
Utility Functions provided by the Angular CLI
The Schematics Collection @schematics/angular
used by the Angular CLI for generating stuff like components or services turns out to be a real gold mine for modifying existing NgModules
. For instance, you find some function to look up modules within @schematics/angular/utility/find-module
. The following table shows two of them which I will use in the course of this article:
Function |
Description |
findModuleFromOptions |
Looks up the current module file. For this, it starts in a given folder and looks for a file with the suffix .module.ts while the suffix .routing.module.ts is not accepted. If nothing has been found in the current folder, its parent folders are searched. |
buildRelativePath |
Builds a relative path that points from one file to another one. This function comes in handy for generating the import statement pointing from the module file to the file with the component to register. |
Another file containing useful utility functions is @schematics/angular/utility/ast-utils
. It helps with modifying existing TypeScript
files by leveraging services provided by the TypeScript compiler. The next table shows some of its functions used here:
Function |
Description |
addDeclarationToModule |
Adds a component, directive or pipe to the declarations array of an NgModule . If necessary, this array is created |
addExportToModule |
Adds an export to the NgModule |
There are also other methods that add entries to the other sections of an NgModule
(addImportToModule
, addProviderToModule
, addBootstrapToModule
).
Please note, that those files are not part of the packages public API. Therefore, they can change in future.
Creating an Rule for adding a declaration to an NgModule
After weve seen that there are handy utility functions, lets use them to build a Rule
for our endeavor. For this, we use a folder utils
with the following two files:

The file add-to-module-context.ts
gets a context class holding data for the planned modifications:
import * as ts from typescript;
export class AddToModuleContext {
// source of the module file
source: ts.SourceFile;
// the relative path that points from
// the module file to the component file
relativePath: string;
// name of the component class
classifiedName: string;
}
In the other file, ng-module-utils.ts
, a factory function for the needed rule is created:
import { ModuleOptions, buildRelativePath } from @schematics/angular/utility/find-module;
import { Rule, Tree, SchematicsException } from @angular-devkit/schematics;
import { dasherize, classify } from @angular-devkit/core;
import { addDeclarationToModule, addExportToModule } from @schematics/angular/utility/ast-utils;
import { InsertChange } from @schematics/angular/utility/change;
import { AddToModuleContext } from ./add-to-module-context;
import * as ts from typescript;
const stringUtils = { dasherize, classify };
export function addDeclarationToNgModule(options: ModuleOptions, exports: boolean): Rule {
return (host: Tree) => {
[...]
};
}
This function takes an ModuleOptions
instance that describes the NgModule
in question. It can be deduced by the options object containing the command line arguments the caller passes to the CLI.
It also takes a flag exports
that indicates whether the declared component should be exported too. The returned Rule
is just a function that gets a Tree
object representing the part of the file system it modifies. For implementing this Rule
Ive looked up the implementation of similar rules within the CLIs Schematics in @schematics/angular
and "borrowed" the patterns found there. Especially the Rule
triggered by ng generated component
was very helpful for this.
Before we discuss how this function is implemented, lets have a look at some helper functions Ive put in the same file. The first one collects the context
information weve talked about before:
function createAddToModuleContext(host: Tree, options: ModuleOptions): AddToModuleContext {
const result = new AddToModuleContext();
if (!options.module) {
throw new SchematicsException(`Module not found.`);
}
// Reading the module file
const text = host.read(options.module);
if (text === null) {
throw new SchematicsException(`File ${options.module} does not exist.`);
}
const sourceText = text.toString(utf-8);
result.source = ts.createSourceFile(options.module, sourceText, ts.ScriptTarget.Latest, true);
const componentPath = `/${options.sourceDir}/${options.path}/`
+ stringUtils.dasherize(options.name) + /
+ stringUtils.dasherize(options.name)
+ .component;
result.relativePath = buildRelativePath(options.module, componentPath);
result.classifiedName = stringUtils.classify(`${options.name}Component`);
return result;
}
The second helper function is addDeclaration
. It delegates to addDeclarationToModule
located within the package @schematics/angular
to add the component to the modules declarations
array:
function addDeclaration(host: Tree, options: ModuleOptions) {
const context = createAddToModuleContext(host, options);
const modulePath = options.module || ;
const declarationChanges = addDeclarationToModule(
context.source,
modulePath,
context.classifiedName,
context.relativePath);
const declarationRecorder = host.beginUpdate(modulePath);
for (const change of declarationChanges) {
if (change instanceof InsertChange) {
declarationRecorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(declarationRecorder);
};
The addDeclarationToModule
function takes the retrieved context information and the modulePath
from the passed ModuleOptions
. Instead of directly updating the module file it returns an array with necessary modifications. These are iterated and applied to the module file within a transaction, started with beginUpdate
and completed with commitUpdate
.
The second helper function is addExport
. It adds the component to the modules exports
array and works exactly like the addDeclaration
:
function addExport(host: Tree, options: ModuleOptions) {
const context = createAddToModuleContext(host, options);
const modulePath = options.module || ;
const exportChanges = addExportToModule(
context.source,
modulePath,
context.classifiedName,
context.relativePath);
const exportRecorder = host.beginUpdate(modulePath);
for (const change of exportChanges) {
if (change instanceof InsertChange) {
exportRecorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(exportRecorder);
};
Now, as weve looked at these helper function, lets finish the implementation of our Rule
:
export function addDeclarationToNgModule(options: ModuleOptions, exports: boolean): Rule {
return (host: Tree) => {
addDeclaration(host, options);
if (exports) {
addExport(host, options);
}
return host;
};
}
As youve seen, it just delegates to addDeclaration
and addExport
. After this, it returns the modified file tree represented by the variable host
.
Extending the used Options Class and its JSON schema
Before we put our new Rule
in place, we have to extend the class MenuOptions
which describes the passed (command line) arguments. As usual in Schematics, its defined in the file schema.ts
. For our purpose, it gets two new properties:
export interface MenuOptions {
name: string;
appRoot: string;
path: string;
sourceDir: string;
menuService: boolean;
// New Properties:
module: string;
export: boolean;
}
The property module
holds the path for the module file to modify and export
defines whether the generated component should be exported too.
After this, we have to declare these additional property in the file schema.json
:
{
"$schema": "http://json-schema.org/schema",
"id": "SchemanticsForMenu",
"title": "Menu Schema",
"type": "object",
"properties": {
[...]
"module": {
"type": "string",
"description": "The declaring module.",
"alias": "m"
},
"export": {
"type": "boolean",
"default": false,
"description": "Export component from module?"
}
}
}
As mentioned in the last blog article, we also could generate the file schema.ts
with the information provided by schema.json
.
Calling the Rule
Now, as weve created our rule, lets put it in place. For this, we have to call it within the Rule
function in index.ts
:
export default function (options: MenuOptions): Rule {
return (host: Tree, context: SchematicContext) => {
options.path = options.path ? normalize(options.path) : options.path;
// Infer module path, if not passed:
options.module = options.module || findModuleFromOptions(host, options) || ;
[...]
const rule = chain([
branchAndMerge(chain([
[...]
// Call new rule
addDeclarationToNgModule(options, options.export)
])),
]);
return rule(host, context);
}
}
As the passed MenuOptions
object is structurally compatible to the needed ModuleOptions
we can directly pass it to addDeclarationToNgModule
. This is the way, the CLI currently deals with option objects.
In addition to that, we infer the module path at the beginning using findModuleFromOptions
.
Testing the extended Schematic
To try the modified Schematic out, compile it and copy everything to the node_modules
folder of an example application. As in the former blog article, Ive decided to copy it to node_modules/nav
. Please make sure to exclude the collections node_modules
folder, so that there is no folder node_modules/nav/node_modules
.
After this, switch to the example applications root, generate a module core
and navigate to its folder:
ng g module core
cd src\app\core
Now call the custom Schematic:

This not only generates the SideMenuComponent
but also registers it with the CoreModule
:
import { NgModule } from @angular/core;
import { CommonModule } from @angular/common;
import { SideMenuComponent } from ./side-menu/side-menu.component;
@NgModule({
imports: [
CommonModule
],
declarations: [SideMenuComponent],
exports: [SideMenuComponent]
})
export class CoreModule { }