Schematics are template based code generators that can transform your project by adding or updating files. You may need to install npm packages and update various files to configure features in Angular applications. Consider you are planning to update your applciation with Angular Material. You need to install material theme files to your application and update angular.json to include the style and scripts. Also you may need to create the various components to design the navigation bar or dashboard. You can simplify these tasks by using schematics. By using angular material schematics, you can automate all these operations of installing packages and generating dashboard or navbar components. Lets try this.
-
Create an Angular workspace using the Angular CLI.
ng new schematics-workspace --create-application false --skips-tests -
This will create an empty workspace without any project. You can now create an Angular project of application type.
ng generate application web-ui-app --routing --style css
-
Run and test the application. You will see the default landing page of the Angular application.
ng serve --open
-
Install the Angular Material theme to the project using theschematics. Run the following command to add and configure the Angular Material. Choose an appropriate color theme while installing.
ng add @angular/material
-
This will update the
angular.jsonfile andpackage.jsonfiles to add the references to the Angular material npm packages. You can now create a navigation bar componenet using the schematics that provides responsive UI for you web application. Run the following command to create the navbar component.ng generate @angular/material:navigation components/navigation
-
Open the
src\app\components\navigation.htmlfile and add<router-outlet>below the<!-- Add Content Here -->line. -
Open the
src\app\app.component.htmlfile and remove all code from it. Add the following line to the file.<app-navigation></app-navigation>
Warning
Ensure the selector of navigation component's selector is app-navigation. If not use the correct selector to add the component in the app.compoent.html file.
- Re-run the project. Stop the project if it is already running. Because you have updated the
angular.jsonfile, it requires a restart. You will be able to see the navigation bar component when the application runs.
You have added a responsive Angular Material navigation bar to the project with less or no effort. All the configurations are done by the Angular Schematics CLI. You can also generate other Angular Material components such as dashboard, address-form etc.
We have seen how Angular Material schematics helped us to easily create and add a navigation bar component with reponsive nature. It created all required files and updated configurations when you run the ng generate command. You can also create such schematics to generate files and updating configurations automatically. Now we will see how the schematics can help us to do so.
-
Install the Schematics CLI in your system.
npm install -g @angular-devkit/schematics-cli
-
This will provide the
schematicscommand globally. You can use this command to create a new schematics project or list schematics from an available schematics project.schematics @angular/material: --list-schematics
-
Create a new schematics project using the following command. Create the project outside the Angular project workspace.
schematics blank sample-schematics
-
This will create a schematics project with the following files.
- package.json
- tsconfig.json
- src\collection.json
- src\sample-schematics\index.ts
- src\sample-schematics\index.spec.ts
The
src\collection.jsonfile contains the list of schematics exported from this package. The default schematics is created inside thesrc\sample-schematicsdirectory. It contains anindex.tsfile that contains the action definition for the schematics. In thecollection.jsonyou can see thesample-schematicsconfiguration."sample-schematics": { "description": "A blank schematic.", "factory": "./sample-schematics/index#sampleSchematics" }
-
Open the
package.jsonfile. You can see a script command to build the schematics project. Add a new script command to run and build the project in watch mode."scripts": { "build": "tsc -p tsconfig.json", "build:watch": "tsc -p tsconfig.json --watch", "test": "npm run build && jasmine src/**/*_spec.js" },
-
Build the project by using the following command in terminal.
npm run build:watch
-
Test the
sample-schematics. Run the following command.schematics .:sample-schematics
-
You can add a new schematics to the existing project using the following command.
schematics blank greeter
-
This will create a new schematics named
greeter. It will create a new folder undersrcwith the namegreeter. Also it updates thecollection.jsonto add the schematics configuration forgreeter. -
Open the
src\greeter\index.tsand update the method to generate ahello.jsfile with a simple greet message.export function greeter(_options: any): Rule { return (tree: Tree, _context: SchematicContext) => { tree.create('hello.js', `console.log('Hello User')`); return tree; }; }
-
Build and run the schematics. Use the following command to run the schematics.
schematics .:greeter
-
The schematics will execute successfully. But it will not create any file in the directory. Because the project run in the dry run state. You can run it without dry run mode.
schematics .:greeter --dry-run falseThe above command will create a
hello.jsfile in the curent directory. -
Now, we need to update the
greeterschematics to accept a user name as input to generate the file with a personalized message. For that you need to define a schema for parameters. Createschema.jsonandschema.d.tsfile in thesrc\greeterfolder. -
Open the
schema.d.tsfile and create an interface to define the list of parameters.export interface Schema{ name:string }
-
Open the
schema.jsonfile and add the following code to define the schema forgreeterschematics. It defines a parameternamethat is accepted while executing thegreeterschematics.{ "$schema": "http://json-schema.org/schema", "$id": "GreeterSchema", "title": "Greeter Schema", "type": "object", "description": "Print a personalized greet message", "properties": { "name": { "type": "string", "description": "Name of the person you want to greet", "$default": { "$source": "argv", "index": 0 } } }, "required": [ "name" ] } -
Open the
collection.jsonto update the schemat definition forgreeter. Add a schema configuration for it."greeter": { "description": "A blank schematic.", "factory": "./greeter/index#greeter", "schema": "./greeter/schema.json" }
-
Open the
index.tsforgreeterand update the_optionsargument type fromanytoSchema. In the function definition you can read the name parameter from the_optionsargument and use it to print the personalized message.import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { Schema } from './schema'; export function greeter(_options: Schema): Rule { return (tree: Tree, _context: SchematicContext) => { let name = _options.name; if(tree.exists('hello.js')){ tree.delete('hello.js'); } tree.create('hello.js', `console.log('Hello ${name}')`); return tree; }; }
-
Build the project and test it. You can run the schematics by using the following command.
schematics .:greeter demouser --dry-run false[!WARNING] Ensure the
hello.jscreated in the previous run is deleted before you run it. Otherwise it will update existinghello.jsand show anUPDATEmessage instead onCREATE.
We can generate files using schematics with the help of template files. The template files are typically stored inside the files folder inside the schematics directory. Angular schematics provides a strings library that provides a set of functions to transform the file and folder names. These functions helps us to easily capitalize, camelize, classify or dasherize the names. For example when we provide a file name as MyDemo, the dasherize function can convert it into my-demo. The camelize function can convert it into myDemo format. To use these functions in files and folders along with the name parameter, you need to use __name@dasherize__.js format. Here the name is the name parameter accepted using schematics command line. Similarly, to camelize the file name, use __name@camelize__.js format. Template files such as *.js, *.ts files can contain interpolations like <% %> and <%= %>. You can print variables in template files using <%= variable %> and execute code using <% code %>.
We can try this in the following demo.
-
Create a folder named
filesinside thegreeterschematics folder. Create a subfolder with the name__name@dasherize__. Create a new file with the name__name@dasherize__.hello.jsinside the subfolder.src |_ greeter |_ files |_ __name@dasherize__ |_ __name@dasherize__.hello.js -
Open the
__name@dasherize__.hello.jsfile and add the following code to it.console.log(`Hello <%= dasherize(name) %> `)
-
Open the
index.tsfile ingreeterfolder and update the code. Update the import statements with the following.import { strings } from '@angular-devkit/core'; import { apply, Rule, SchematicContext, Tree, url, template, mergeWith } from '@angular-devkit/schematics'; import { Schema } from './schema';
-
Also update the
greetermethod to use the template files to generate the file.export function hello(_options: Schema): Rule { return (tree: Tree, _context: SchematicContext) => { const sourceTemplate = url("./files"); const parameterizedTemplate = apply(sourceTemplate,[ template({ ..._options, ...strings }) ]); tree = mergeWith(parameterizedTemplate)(tree,_context) as Tree; return tree; }; }
We are passing the
..._optionsand...stringsto thetemplatemethod. It is used to pass the schema variables and the string utility methods such as dasherize, capitalzie, camelize and classify to the template file. So we can use those parameter variables and string functions in out template file. -
Build the project. Run the following commands to test the schematics.
schematics .:greeter DemoUser --dry-run falseschematics .:greeter sampleUser --dry-run falseschematics .:greeter dummy_user --dry-run false -
You will be able to see the directories created for each user name and corresponding dasherized files inside it.
We have now understood the use of template file and the file name transformations using string methods. We can now try to create an Angular service class to provide some http operations.
-
Create a new schematics using the following command.
schematics blank http-resource
-
This will create a new schematics and add it to the
collection.json. You can now create a schema file to accept the paramters required for thehttp-resourceschematics. Addschema.jsonandschema.d.tsfile to the schematics folder. -
Open the
schema.d.tsfile and add the following code.export interface Schema{ name:string; url:string; }
-
Define the JSON schema in the
schema.jsonfile. Add the following lines to it.{ "$schema": "http://json-schema.org/schema", "$id": "HttpResource", "title": "Http Resource", "type": "object", "description": "Service class to perform http operations", "properties": { "name": { "type": "string", "description": "Name of the http service class", "$default": { "$source": "argv", "index": 0 } }, "url": { "type": "string", "description": "Base Url of the API service", "x-prompt": "What is the API base url (eg: http://domain.com/api/resource)?" } }, "required": [ "name" ] } -
Update the schematics definition in
collection.jsonto define the schema property for thehttp-resourceschematics."http-resource": { "description": "A blank schematic.", "factory": "./http-resource/index#httpResource", "schema": "./http-resource/schema.json" }
-
Now, we need to create the template files for the schematics. Create
filesdirectory inside thehttp-resourcedirectory and create a subfolder with the name__name@dasherize__. Add two files to it - one model interface and an angular service class. Create__name@dasherize__.tsand__name@dasherize__.service.tsfiles inside the subfolder. -
Open
__name@dasherize__.tsfile and add the following code to it.export interface <%= classify(name) %>{ id:number; }
-
Add the following code to
__name@dasherize__.service.tsfile.import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import { Observable} from 'rxjs'; import {<%= classify(name) %>} from './<%= dasherize(name) %>' const API_URL= '<%= url %>'; @Injectable({ providedIn:'root' }) export class <%= classify(name)%>CrudService{ constructor(private http:HttpClient){ } findAll():Observable<<%=classify(name) %>[]>{ return this.http.get<<%=classify(name)%>[]>(API_URL); } }
-
Update the
index.tsto generate the files using templates.import { strings } from '@angular-devkit/core'; import { apply, Rule, SchematicContext, Tree, url, template, mergeWith } from '@angular-devkit/schematics'; import { Schema } from './schema'; export function httpResource(_options: Schema): Rule { return (tree: Tree, _context: SchematicContext) => { const sourceTemplate = url("./files"); const parameterizedTemplate = apply(sourceTemplate, [ template({ ..._options, ...strings }) ]); tree = mergeWith(parameterizedTemplate)(tree, _context) as Tree; return tree; }; }
-
Build the project and run the schematics using the following command.
schematics .:http-resource employee --url http://somedomain.com/api/employee --dry-run falseYou will be able to see the
employeefolder and the service class and model interface file inside it. -
Now, we can add an optional parameter to the schematics. For that we will use a
findOneflag that creates afindOne(id:number)method in the service class if true otherwise not. Update theSchemainterface to addfindOneboolean member.export interface Schema{ name:string; url:string; findOne:boolean; }
-
Update the
schema.jsonto define thefindOneparameter."findOne":{ "type":"boolean", "description": "True if want to generate a findOne method else false", "default":false }
-
Open
__name@dasherize__.service.tsfile and add the following code below thefindAllmethod to generate thefindOnemethod based on the boolean parameterfindOne.<% if(findOne){ %> findOne(id:number):Observable<<%=classify(name)%>>{ return this.http.get<<%=classify(name)%>>(`${API_URL}/${id}`) } <% } %>
-
Build the project and test with the following commands.
schematics .:http-resource employee --url http://somedomain.com/api/employee --findOne --dry-run falseschematics .:http-resource product --url http://somedomain.com/api/products --dry-run false
We have created and tested the schematics successfully. But we have not yet used them inside an Angular project. To use the schematics with angular projects you need to install the @schematics/angular package to the schematics project. This will provide the utility methods and classes required to access the Angular project workspace and projects. Install the @schematics/angular with the following command.
```bash
npm i @schematics/angular
```
-
If you want to use your schematics with Angular projects, you need to have the project name and path. You can pass the project name using the command line arguments and path is generated from the project name value. So we need to define
projectandpathparameters in our schema definition file. -
Open the
schema.d.tsfile and add the following two members to theSchemainterface.path:string; project:string;
-
Also, you need to update the
schema.jsonfile for the new parameters. Add the following lines after thefindOneparameter."path": { "type": "string", "format": "path", "description": "The path at which to create the service, relative to the workspace root.", "visible": false }, "project": { "type": "string", "description": "The name of the project.", "$default": { "$source": "projectName" } }
-
Open the
index.tsfile of thehttp-resourceschematics. Add the following import statements to it.import { Rule, SchematicContext, Tree, url, apply, template, mergeWith, chain, MergeStrategy, SchematicsException, move } from '@angular-devkit/schematics'; import { strings } from '@angular-devkit/core'; import { createDefaultPath, getWorkspace } from '@schematics/angular/utility/workspace'; import { parseName } from '@schematics/angular/utility/parse-name';
-
Update the index method with the following code:
export function httpResource(_options: Schema): Rule { return async (tree: Tree, _context: SchematicContext) => { const workspace = await getWorkspace(tree); if (!_options.project) { _options.project = workspace.projects.keys().next().value; } const project = workspace.projects.get(_options.project); if (!project) { throw new SchematicsException(`Invalid project name: ${_options.project}`); } if (_options.path === undefined) { _options.path = await createDefaultPath(tree, _options.project as string); } const parsedPath = parseName(_options.path, _options.name); _options.name = parsedPath.name; _options.path = parsedPath.path; const sourceTemplate = url("./files"); const sourceParameterizedTemplate = apply(sourceTemplate, [ template({ ..._options, ...strings }), move(parsedPath.path) ]); return chain([mergeWith(sourceParameterizedTemplate, MergeStrategy.Overwrite)]); }; }
We are converting the anonymous function that is returned by
httpResourcemethod is converted into async because we are calling some awaitable methods inside the function.const workspace = await getWorkspace(tree);
The above line returns the current Angular project workspace reference object. An Angular project workspace can contain multiple projects.
if (!_options.project) { _options.project = workspace.projects.keys().next().value; } const project = workspace.projects.get(_options.project); if (!project) { throw new SchematicsException(`Invalid project name: ${_options.project}`); }
The above piece of code checks for the project name in the command line parameters. The command line parameters are accessible using
_optionsvariable. If project name is not explicitly passed through the command line parameters then it takes the default project name from the angular project workspace. Using the project name it generate the project reference from the projects collection of the workspace object. If project reference is undefined then it throws an error and terminate the schematics execution.if (_options.path === undefined) { _options.path = await createDefaultPath(tree, _options.project as string); } const parsedPath = parseName(_options.path, _options.name); _options.name = parsedPath.name; _options.path = parsedPath.path;
The above code checks for the project path value in the options parameters. If not found it uses the
createDefaultPathmethod to generate the path from the current project name. Then it parse the project path and name and store it to_optionsparameters set.const sourceTemplate = url("./files"); const sourceParameterizedTemplate = apply(sourceTemplate, [ template({ ..._options, ...strings }), move(parsedPath.path) ]); return chain([mergeWith(sourceParameterizedTemplate, MergeStrategy.Overwrite)]);
The above lines of code uses the templates files in
filesdirectory to generate the Angular service and model class. Thechainmethod asynchronously compile and build the files and move them to the specified path.MergeStrategy.Overwriteensures that if file is already present then it will be overwritten by the new files. -
Build the project.
-
There are different ways to test the schematics with the Angular project.
-
Method 1: Open the command terminal in the angular project folder and run the following command. We assume that the schematics project and angular workspace are in same directory.
schematics ../sample-schematics/src/collection.json:http-resource services/employee --dry-run false -
Method 2: Use the
npm linkcommand to link your angular project and schematics project. It is very easy to test schematics without rebuilding and installing to the angular project. As the first step open thepackage.jsonfile of the schematics project and update the name parameter value to@sample/schematics. Rebuild your schematics project and run the following command from angular project folder.npm link ../sample-schematics
This will install
@sample/schematicspackage to angular project. You can verify it innode_modulesfolder. Then run the following command to create the http service using schematics.ng generate @sample/schematics services/employee --url http://somedomain.com/api/employees --findOne
-
Method 3: You can package the schematics project using the
npm packcommand and install it to the Angular project using thenpm installcommand. For that, open thepackage.jsonfile of the schematics project and update thename,versionanddescriptionif necessary. Also, open the.npmignorefile and comment the following lines.*.ts !*.d.ts
Then open command terminal in the schematics project path and build the project using
npm run build
Then package the project using the following command.
npm pack
This will generate the
.tgzfile. You can install thistgzfile using thenpm installcommand in Angular project. For that open the terminal in Angular project path and run the following command.npm install <path-to-tgz-file>
You can see the npm package is installed in
node_modulesfolder and updated in the dependencies list ofpackage.json. Run the following command to generate the service using schematics.ng generate @sample/schematics:http-resource services/employee --url http://somedomain.com/api/employees --find-one
-
Method 4: You can also publish the package to
https://npmjs.comsite (npm repository) and install it to the Angular project from anywhere. For that you need to update thepackage.jsonfile of the schematics project and provide a unique name for it. Also you can update the version number and description. Addprivate:falseattribute topackage.json. Then login to your npm account using the following command:npm login
If you don't have the npm account you can create a new one using
npm addusercommand. After creating a new account you need to verify the account using the link received in the registered e-mail.Then build the schematics project by running the following command
npm run build
Then publish it to the npm repository using:
npm publish
[!IMPORTANT] Ensure the name in the package.json is unique. Otherwise you may receive an
ACCESS DENIEDerror while publishing it.Now, You can install the package to Angular project using the
npm installcommand.npm install --save @sample/schematics
Later you can use the
ng generatecommand to create the service using schematics.ng generate @sample/schematics:http-resource services/employee --url http://somedomain.com/api/employees --find-one
-
You can use the ng add schematics to generate the servcie class and model interface which we have defined using the http-resource schematics. Ng Add is a common practice of generating/updating files in Angular projects. You can achieve this by adding a new schematics ng-add to your sample-schematics project.
schematics blank ng-addOpen the collections.json and update the schematics definition for ng-add. Add the schema property and set the schema.json file of the http-resource schematics as its value.
"ng-add": {
"description": "Ng Add Schematics for http-resource",
"factory": "./ng-add/index#ngAdd",
"schema": "./http-resource/schema.json"
}Open the index.ts file of the ng-add schematics and update the import statement.
import { chain, Rule, schematic, SchematicContext, Tree } from '@angular-devkit/schematics';Update the ngAdd method definition with the following code. It will execute the http-resource schematic when you run the ng-add schematic.
export function ngAdd(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return chain([schematic('http-resource', _options)])(tree, _context);
};
}Build the project using the following command.
npm run buildPublish the package using npm pack and install it to Angular project. Alternatively, you can publish the package to npm repository using npm publish command. Then open the terminal in Angular project and run the following command to generate a service class for product using ng add command.
ng add @sample/schematics services/employee --url http://somedomain.com/api/employees --find-oneYou can see the service class and the model interface are created in the project folder.