The Angular CLI is a very powerful tool and the de facto solution for working with Angular applications. It eliminates most of the boilerplate code and configuration from the developer and allows them to focus on the fun stuff, which is building awesome Angular applications. Apart from enhancing the Angular development experience, it can be easily customized to the needs of each developer.
The Angular CLI contains a set of useful commands for building, bundling, and testing Angular applications. It also provides a collection of special commands, called schematics, that are used to generate various Angular artifacts such as components, modules, and services. Schematics expose a public API that developers can use to create their own Angular CLI commands or extend the existing ones.
In this chapter, we will cover the following details about schematics:
Angular schematics are libraries that can be installed using npm. They are used in various situations, including creating components that share a standard user interface or even enforcing conventions and coding guidelines inside an organization. A schematic can be used standalone or as a companion for an existing Angular library.
Angular schematics are packaged into collections, and they reside in the @schematics/angular npm package. When we use the Angular CLI to run the ng add or the ng build command, it runs the appropriate schematic from that package. The Angular CLI currently supports the following types of schematics:
In this project, we will focus on generate schematics, but the same rules apply to all the other commands.
In this project, we will learn how to use the schematics API to build custom Angular CLI generate schematics for creating components and services. First, we will build a schematic for creating an Angular component that uses the Tailwind CSS framework in its template. Then, we will create a schematic to generate an Angular service that injects the built-in HTTP client by default and creates one method for each HTTP request in a CRUD operation.
Build time: 1 hour
The following prerequisites and software tools are required to complete this project:
The schematics CLI is a command-line interface that we can use to interact with the schematics API. To install it, run the following npm command:
npm install -g @angular-devkit/schematics-cli
The preceding command will install the @angular-devkit/schematics-cli npm package globally on our system. We can then use the schematics executable to create a new schematics collection:
schematics blank my-schematics
The previous command will generate a schematics project called my-schematics. It contains a schematic with the same name by default inside the src folder. A schematic contains the following files:
The JSON schema file of the collection contains one entry for each schematic associated with that collection:
collection.json
{
"$schema": "../node_modules/@angular-
devkit/schematics/collection-schema.json",
"schematics": {
"my-schematics": {
"description": "A blank schematic.",
"factory": "./my-schematics/index#mySchematics"
}
}
}
Each schematic in the collection contains a short description, as indicated by the description property, and a factory property that points to the main entry point of the schematic using a special syntax. It contains the filename ./my-schematics/index, followed by the # character, and the name of the function exported by that file, named mySchematics.
The main entry point of a schematic contains a rule factory method that is exported by default and returns a Rule object:
index.ts
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// You don't have to export the function as default.
// You can also have more than one rule factory
// per file.
export function mySchematics(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
};
}
A schematic does not interact directly with the filesystem. Instead, it creates a virtual filesystem that is represented by a Tree object. The virtual filesystem contains a staging area where all transformations from schematics happen. This area aims to make sure that any transformations that are not valid will not propagate to the actual filesystem. As soon as the schematic is valid to execute, the virtual filesystem will apply the changes to the real one. All transformations of a schematic operate in a SchematicContext object.
In the following section, we will learn how to use the schematics CLI and create a component generation schematic.
Tailwind is a very popular CSS framework that enforces a utility-first core principle. It contains classes and styles that can be used in Angular applications to create easily composable user interfaces.
We will use the schematics API of the Angular CLI to build a generation schematic for Angular components. The schematic will generate a new Angular component that is styled with a Tailwind container layout.
Important note
The schematic that we will build does not need to have Tailwind CSS installed by default. However, the application in which we will use the schematic does require it.
Let's see how we can accomplish that:
schematics blank tailwind-container
The preceding command will update the collection.json file to contain a new entry for the tailwind-container schematic. It will also create a tailwind-container folder in the src folder of our workspace.
{
"$schema": "http://json-schema.org/schema",
"id": "TailwindContainerSchema",
"title": "My Tailwind Container Schema",
"type": "object",
"properties": {
"name": {
"description": "The name of the component.",
"type": "string"
},
"path": {
"type": "string",
"format": "path",
"description": "The path to create the
component.",
"visible": false
}
},
"required": ["name"]
}
Each schematic can have a JSON schema file that defines the options that are available when running the schematic. Since we want to create a component generation schematic, we need a name and a path property for our component. Each of these properties has metadata associated with it, such as the type and the description. The name of the component is required when invoking the schematic as indicated by the required array property.
{
"$schema": "../node_modules/@angular-
devkit/schematics/collection-schema.json",
"schematics": {
"my-schematics": {
"description": "A blank schematic.",
"factory": "./my-schematics/index#mySchematics"
},
"tailwind-container": {
"description": "Generate a Tailwind container
component",
"factory": "./tailwind-
container/index#tailwindContainer",
"schema": "./tailwind-container/schema.json"
}
}
}
In the preceding file, we set a proper description for our schematic. We also add the schema property that points to the absolute path of the schema.json file we created in step 3.
export interface Schema {
name: string;
path: string;
}
The preceding file defines the Schema interface that contains mapping properties to those defined in the schema.json file.
We have now created all the underlying infrastructure that we will use to create our schematic. Let's see how to write the actual code that will run our schematic:
<div class="container mx-auto">
</div>
The preceding file denotes the template of the component that our schematic will generate. The __name prefix will be replaced by the name of the component that we will pass as an option in the schematic. The @dasherize__ syntax indicates that the name will be separated with dashes and converted to lowercase if passed in camel case.
import { Component } from '@angular/core';
@Component({
selector: 'my-<%= dasherize(name) %>',
templateUrl: './<%= dasherize(name)
%>.component.html'
})
export class My<%= classify(name) %>Component {}
The preceding file contains the TypeScript class of the component that will be generated. The selector and the templateUrl properties of the @Component decorator are built using the dasherize method and the name of the component. The name of the class contains a different method called classify that takes the name of the component as a parameter and converts it to title case.
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { Schema } from './schema';
// You don't have to export the function as default.
// You can also have more than one rule factory
// per file.
export function tailwindContainer(_options: Schema): Rule {
return (_tree: Tree, _context: SchematicContext) => {};
}
import { normalize, strings } from '@angular-devkit/core';
import { apply, applyTemplates, chain, mergeWith, move, Rule, SchematicContext, Tree, url } from '@angular-devkit/schematics';
import { Schema } from './schema';
_options.path = _options.path ?? normalize('src/app/' + _options.name as string);
const templateSource = apply(url('./files'), [
applyTemplates({
classify: strings.classify,
dasherize: strings.dasherize,
name: _options.name
}),
move(normalize(_options.path as string))
]);
In the preceding code, first, we set the path property of the component in case one is not passed in the schematic. By default, we create a folder inside the srcapp folder that has the same name as the component. We then use the apply method to read the template files from the files folder and pass the dasherize, classify, and name properties using the applyTemplates function. Finally, we call the move method to create the generated component files in the provided path.
return chain([
mergeWith(templateSource)
]);
In the preceding snippet, we call the chain method to execute our schematic, passing the result of the mergeWith function that uses the templateSource variable we created in step 6.
Now we can go ahead and test our new component schematic:
npm run build
The preceding command will invoke the TypeScript compiler and transpile the TypeScript source files into JavaScript. It will generate the JavaScript output files into the same folders, side by side, with the TypeScript ones.
npm link
The preceding command is used so that we can install the schematics later without querying the public npm registry.
ng new my-app --defaults
npm link my-schematics
The previous npm command will install the my-schematics library into the current Angular CLI workspace.
Tip
The link command is like running npm install my-schematics, except that it downloads the npm package from the global npm cache of our machine and does not add it to the package.json file.
ng generate my-schematics:tailwind-container --name=dashboard
In the preceding command, we use our custom schematic by passing the name of our collection, my-schematics, followed by the specific schematic name, tailwind-container, separated by a colon. We also pass a name for our component using the --name option of the schematic.
We can verify that our schematic worked correctly by observing the output in the terminal:
We have successfully created a new schematic that we can use for crafting custom Angular components according to our needs. The schematic that we built generates a new Angular component from scratch. Angular CLI is so extensible that we can use it to hook into the execution of built-in Angular schematics and modify them accordingly.
In the following section, we will investigate this by building a schematic for Angular HTTP services.
For our schematics library, we will create a schematic that scaffolds an Angular service. It will generate a service that imports the built-in HTTP client. It will also contain one method for each HTTP request that is involved in a CRUD operation.
The generation schematic that we are going to build will not stand on its own. Instead, we will combine it with the existing generation schematic of the Angular CLI for services. Thus, we do not need a separate JSON schema.
Let's get started by creating the schematic:
schematics blank crud-service
"crud-service": {
"description": "Generate a CRUD HTTP service",
"factory": "./crud-service/index#crudService"
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class <%= classify(name) %>Service {
constructor(private http: HttpClient) { }
}
The preceding file is the template of the Angular service file that our schematic will generate. It injects the HttpClient service in the constructor of the class by default.
apiUrl = '/api';
create(obj) {
return this.http.post(this.apiUrl, obj);
}
read() {
return this.http.get(this.apiUrl);
}
update(obj) {
return this.http.put(this.apiUrl, obj);
}
delete(id) {
return this.http.delete(this.apiUrl + id);
}
Creating all the methods beforehand eliminates much of the boilerplate code. The developer that uses the schematic will only need to modify these methods and add the actual implementation for each one.
We have almost finished our schematic except for creating the factory function that will invoke the generation of the service:
import { normalize, strings } from '@angular-devkit/core';
import { apply, applyTemplates, chain, externalSchematic, MergeStrategy, mergeWith, move, Rule, SchematicContext, Tree, url } from '@angular-devkit/schematics';
export function crudService(_options: any): Rule {
return (_tree: Tree, _context: SchematicContext) =>
{};
}
const templateSource = apply(url('./files'), [
applyTemplates({
..._options,
classify: strings.classify,
dasherize: strings.dasherize
}),
move(normalize(_options.path ??
normalize('src/app/')))
]);
The previous snippet looks identical to the one that we used for our component schematic. The main differences are that the default path is the srcapp folder and that we pass all available options using the _options parameter to the schematic.
Important note
It is not possible to know which options will be used to generate the Angular service beforehand. Thus, we use the spread operator to pass all available options to the templateSource method. That is also the reason that the _options parameter is of type any.
return chain([
externalSchematic('@schematics/angular', 'service',
_options),
mergeWith(templateSource, MergeStrategy.Overwrite)
]);
In the preceding statement, we first use the externalSchematic method to call the built-in generation schematic for creating Angular services. Then, we merge the result from executing that schematic with our templateSource variable. We also define the strategy of the merge operation using MergeStrategy.Overwrite so that any changes made by our schematic will overwrite the default ones.
Our schematic for creating CRUD services is now complete. Let's use it in our sample application:
npm run build
Tip
We do not need to link the schematics library again. Our application will be automatically updated as soon as we make a new build of our schematics.
ng generate my-schematics:crud-service --name=customers
We use the generate command of the Angular CLI, passing the name of our schematics collection again but targeting the crud-service schematic this time.
Notice that the schematic has generated a unit test file for us automatically. How is this possible? Well, recall that we merged our schematic with the built-in generate schematic of the Angular CLI. So, whatever the default schematic does, it reflects directly to the execution of the custom schematic.
We have just added a new helpful command to our schematics collection. We can generate an Angular service that interacts with HTTP endpoints. Moreover, we have added the fundamental methods that will be needed for communicating with the endpoint.
In this project, we used the schematics API of the Angular CLI to create custom schematics for our needs. We built a schematic for generating Angular components that contain Tailwind CSS styles in their template. We also built another schematic that creates an Angular service to interact with the built-in HTTP client. The service contains all the necessary artifacts for working with an HTTP CRUD application.
The Angular CLI is a flexible and extensible tool that enhances the development experience dramatically. The imagination of each developer is all that limits what can be done with such an asset in their toolchain. The CLI, along with the Angular framework, allows developers to create excellent web applications.
As we have learned throughout this book, the popularity of the Angular framework in the web developer world is so great that it is straightforward to integrate it today with any technology and create fast and scalable Angular applications. So, we encourage you to get the latest version of Angular and create amazing applications today.
Create a template for the unit test file that is generated when running the crud-service schematic. The file should be placed in the files folder of the related schematic. It should configure the testing module to use HttpClientTestingModule and HttpTestingController. It should also contain one unit test for each method of the service.
You can find the solution to the exercise in the GitHub repository indicated in the Getting started section.
100.24.12.23