Brian Love
Google Developer Expert in Angular, software engineer and skier located in Denver, CO

Angular Schematics Tutorial

Reading time ~6 minutes

Learn how to create Angular schematics.

What are Schematics?

Schematics are generators that transform an existing filesystem.

With schematics we can:

  • Create files,
  • Refactor existing files, or
  • Move files around.

What can Schematics do?

In general, schematics:

  • Add libraries to an Angular project,
  • Update libraries in an Angular project, and
  • Generate code.

The possibilities for using schematics in your own projects or at your organization is endless. A few examples of how you or your organization can benefit from creating a schematics collection include:

  • Generating commonly used UI patterns in an application.
  • Generating organization specific components using predefined templates or layouts.
  • Enforcing organizational architecture.

Angular Specific?

Currently, yes, schematics are part of the Angular ecosystem.

CLI Integration?

Yes, schematics are tightly integrated with the Angular CLI. You use schematics with the following CLI commands (to name a few):

  • ng add
  • ng generate
  • ng update

What is a Collection?

A collection is a list of schematics. We’ll define the metadata for each schematic in our project’s collection.json file.

Installation

Ok, let’s dive in. 🏄‍ 🏄‍ First, install the schematics CLI via npm or yarn:

$ npm install -g @angular-devkit/schematics-cli
$ yarn add -g @angular-devkit/schematics-cli

Getting Started

To get started, I would recommend that you check out the sample schematics and read through the code and associated comments. You can generate the sample schematics via:

$ schematics schematic --name demo

Hello World Demo

Check out the repository to follow along as we build a simple Hello World schematic:

Hello World

A good place to start is with a simple “Hello World” schematic. Create a new schematics blank project using the schematics CLI:

$ schematics blank --name=hello-world
$ cd hello-world

Open the src/collection.json file:

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "hello-world": {
      "description": "A blank schematic.",
      "factory": "./hello-world/index#helloWorld"
    }
  }
}

Let’s review the collection.json configuration:

  • The $schema property specifies the schema for validation.
  • The schematics property is an object where each key is the name of a schematic.
  • Each schematic has a description property that, you guessed it, provides a description of the schematic.
  • Each schematic also has a factory property that is a string that specifies the file location of our schematic’s entry point, followed by a hash symbol, followed by the function that will be invoked; in this case we’ll invoke the helloWorld() function in the hello-world/index.js file.

We can also specify some additional properties for a schematic:

  • The optional aliases property is an array that we can use to specify one or more aliases for our schematic. For example, an alias for the “generate” schematic that ships with the Angular cli is “g”. This enables us to execute the generate command via $ ng g.
  • The optional schema property can be used to specify a schema for each individual schematic and all of the command line options that are available for the schematic.

It’s also important to note that our project’s package.json file contains a new schematics property that points to the src/collections.json file:

{
  "name": "hello-world",
  "version": "0.0.0",
  "description": "A blank schematics",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "test": "npm run build && jasmine src/**/*_spec.js"
  },
  "keywords": [
    "schematics"
  ],
  "author": "",
  "license": "MIT",
  "schematics": "./src/collection.json",
  "dependencies": {
    "@angular-devkit/core": "^7.0.2",
    "@angular-devkit/schematics": "^7.0.2",
    "@types/jasmine": "^2.6.0",
    "@types/node": "^8.0.31",
    "jasmine": "^2.8.0",
    "typescript": "^2.5.2"
  }
}

Entry Function

Let’s open the src/hello-world/index.ts file and examine the contents:

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export function helloWorld(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    return tree;
  };
}
  • We export the helloWorld function that will be invoked as the “entry function”.
  • The function accepts an options argument, which is an object of command line argument key/value pairs.
  • Our function is a higher order function, meaning that it either accepts or returns a function reference. In this case, our function returns a function that accepts the Tree and the SchematicContext objects.

What is a Tree?

According to the documenation, a Tree is:

A staging area for changes, containing the original file system, and a list of changes to apply to it.

A few of things we can use the tree to accomplish include:

  • read(path: string): Buffer | null; - read the path specified
  • exists(path: string): boolean; - determine if a path exists
  • create(path: string, content: Buffer | string): void; - creates a new file at the specified path with the specified content
  • beginUpdate(path: string): UpdateRecorder; - returns a new UpdateRecorder instance for the file at the specified path
  • commitUpdate(record: UpdateRecorder): void; - commits the actions of an UpdateRecorder

We’ll cover the UpdateRecorder later in this post.

What is a Rule?

A Rule is a function that applies actions to a Tree given the SchematicContext:

declare type Rule = 
 (tree: Tree, context: SchematicContext) => 
 Tree | Observable<Tree> | Rule | void;

In the hello world example code above, note that the helloWorld() “entry” function returns a Rule.

Build and Execute

Let’s go ahead and build and execute our schematics:

$ yarn build
$ schematics .:hello-world --foo=bar

A few things to note:

  • We are executing the schematics using the schematics CLI, not the Angular CLI.
  • The first option is to specify the collection name. In this instance we are pointing to the current directory, ., as the collection.
  • The collection name optional. If we omit the collection name the internal collection is used. We used the internal collection previously when we executed the “blank” schematic.
  • If we specify a collection name, we use the colon ( : ) to separate the collection name followed by the schematic name.
  • In this example we are executing the “hello-world” schematic.
  • Finally, we also specify the “foo” option to our schematic, which has the value “bar”.

Let’s start by adding a log statement to the function that we return from the helloWorld function:

export function helloWorld(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    console.log(options);
    return tree;
  };
}

Build and execute the schematic and you should now see the logging of the options that you specify to the schematic:

$ yarn build
$ schematics .:hello-world --foo=bar
{ foo: 'bar' }
Nothing to be done.

Generate a Hello World Template

Our schematic doesn’t no anything right now other than log out the user specified options. Let’s update our Rule function to use the tree.create() method to create a new file:

export function helloWorld(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    tree.create("hello-world.html", `<h1>Hello ${options.name} 👋</h1>`);
    return tree;
  };
}

As we learned previously, the create() method accepts the path to the file we are creating along with the content for the file. In this example, we’re simply using the name value that the user specified to populate a hello-world.html template with the string “Hello” followed by the name.

When we build and execute our updated schematic we should see:

$ yarn build
$ schematics .:hello-world --name="Angular Community" --dry-run
CREATE /hello-world.html (37 bytes)

Note:

  • I specified the name option when executing the schematic
  • I also specified the dry-run option to avoid the actual file creation.

Removing the dry-run option should create the hello-world.html template in your current working directory.

Unit Tests

Angular schematics include a SchematicTestRunner for building a suite of unit tests to ensure the quality of your schematics collection.

Let’s test our newly created hello-world schematic:

const collectionPath = path.join(__dirname, "../collection.json");

describe("hello-world", () => {
 it("works", () => {
   const runner = new SchematicTestRunner("schematics", collectionPath);
   const tree = runner.runSchematic("hello-world", {}, Tree.empty());

   expect(tree.files.length).toEqual(1);
 });
});
  • The test is executed using Jasmine, which should feel very comfortable to Angular developers.
  • We new-up the SchematicTestRunner class, specifying the collectionName followed by the collectionPath.
  • Using the test runner we invoke runSchematic(), specifying the schematicName, the options object and the source Tree.
  • Finally, we assert that the tree’s file count has incremented to 1.

Conclusion

I hope this is a start to your learning journey with Angular Schematics.

For some further resources, check out:

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).