Chapter 10: Customizing Angular CLI Commands Using Schematics

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:

  • Installing the schematics CLI
  • Creating a Tailwind component
  • Creating an HTTP service

Essential background theory and context

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:

  • Add schematic: This is used to install an Angular library in an Angular CLI workspace using the ng add command.
  • Update schematic: This is used to update an Angular library using the ng update command.
  • Generate schematic: This is used to generate Angular artifacts in an Angular CLI workspace using the ng generate command.

In this project, we will focus on generate schematics, but the same rules apply to all the other commands.

Project overview

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

Getting started

The following prerequisites and software tools are required to complete this project:

Installing the schematics CLI

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:

  • collection.json: A JSON schema that describes the schematics that belong to the my-schematics collection
  • my-schematicsindex.ts: The main entry point of the my-schematics schematic
  • my-schematicsindex_spec.ts: The unit test file of the main entry point of the my-schematics schematic

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.

Creating a Tailwind component

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:

  1. Execute the following schematics CLI command to add a new schematic to our collection:

    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.

  2. Create a schema.json file inside the tailwind-container folder and add the following content:

    {

        "$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.

  3. Open the collection.json file and set the properties of the tailwind-container schematic as follows:

    {

      "$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.

  4. Create a schema.ts file inside the tailwind-container folder and add the following content:

    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:

  1. Create a folder named files inside the tailwind-container folder.
  2. Create a file called [email protected]__.component.html.template inside the files folder and add the following contents:

    <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.

  3. Create a file called [email protected]__.component.ts.template and add the following contents:

    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.

  4. Open the index.ts file of the tailwind-container folder, set the type of options to Schema and remove the return statement:

    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) => {};

    }

  5. Add the following import statements at the top of the file:

    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';

  6. Insert the following code inside the tailwindContainer function:

    _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.

  7. Add the following statement at the end of the factory function:

    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:

  1. Execute the following npm command to build the 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.

  2. Run the following command to install the schematics library into our global npm cache:

    npm link

    The preceding command is used so that we can install the schematics later without querying the public npm registry.

  3. Execute the following Angular CLI command in a folder of your choice outside the workspace to scaffold a new Angular application with default options:

    ng new my-app --defaults

  4. Navigate to the my-app folder and run the following command to install our schematics:

    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.

  5. Use the generate command of the Angular CLI to create a dashboard component:

    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:

Figure 10.1 – Generate Angular component

Figure 10.1 – Generate Angular component

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.

Creating an HTTP service

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:

  1. Execute the following command to add a new schematic to our collection:

    schematics blank crud-service

  2. Open the collection.json file and provide an explanatory description for the schematic:

    "crud-service": {

      "description": "Generate a CRUD HTTP service",

      "factory": "./crud-service/index#crudService"

    }

  3. Create a folder named files inside the crud-service folder of the workspace.
  4. Create a file named [email protected]__.service.ts.template inside the files folder and add the following code:

    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.

  5. Define a service property that will represent the URL of the API with which we want to communicate:

    apiUrl = '/api';

  6. Add the following methods for each HTTP request of a CRUD operation:

    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:

  1. Open the index.ts file of the crud-service folder and add the following import statements:

    import { normalize, strings } from '@angular-devkit/core';

    import { apply, applyTemplates, chain, externalSchematic, MergeStrategy, mergeWith, move, Rule, SchematicContext, Tree, url } from '@angular-devkit/schematics';

  2. Rename the tree parameter and remove it from the return statement because we will not use it. The resulting factory function should look like the following:

    export function crudService(_options: any): Rule {

      return (_tree: Tree, _context: SchematicContext) =>

        {};

    }

  3. Add the following snippet in the crudService function:

    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.

  4. Add the following return statement at the end of the function:

    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:

  1. Execute the following command to build the schematics library:

    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.

  2. Navigate to the my-app folder in which our application resides.
  3. Execute the following command to generate an Angular service using our new schematic:

    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.

  4. The new Angular service is created in the srcapp folder as indicated by the output in the terminal window:

Figure 10.2 – Generating an Angular service

Figure 10.2 – Generating an Angular service

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.

Summary

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.

Exercise

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.

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset