Extend Angular Schematics to customize your development process

Post Editor

Have you ever realized that you often repeat the same patterns across multiple files? Creating schematics that override well-known Angular schematics, designing them based on project needs will enhance your development and reduce time spent on generating files.

12 min read
0 comments
post

Extend Angular Schematics to customize your development process

Have you ever realized that you often repeat the same patterns across multiple files? Creating schematics that override well-known Angular schematics, designing them based on project needs will enhance your development and reduce time spent on generating files.

post
post
12 min read
0 comments
0 comments

Have you ever realized that you often repeat the same patterns across multiple files, classes? It could be a simple task like adding a ngOnDestroy method to every component you create to manage subscriptions or adding HttpClient to almost every service you make. I bet you have!

I can assure you, there is nothing wrong with that - this will not be an article about DRY and how to remove duplicates in your project using OOP tricks. Sometimes creating a complex, nested architecture just to remove some duplicated elements is not enough or is not the best you can do - simplicity and readability of your classes are as important. This is a scenario where Angular Schematics are becoming incredibly useful.

Schematics
Link to this section

I think every Angular developer uses Angular Schematics, at least when creating components or services, but if you are not familiar with this valuable tool - quick explanation:

Angular Schematics offers dozens of commands that you could use to generate files, run migrations, update files, etc. For instance, to create a component directory, along with the template, stylesheet, and component class file, you can simply run ng generate component my-component in the terminal or use the short version ng g c my-component. Schematics will take care of everything and produce three files with example code. Pretty helpful, right?

Why should I be interested?
Link to this section

What if I told you that, in many projects, developers need to create files almost every time with some custom configuration? To better explain that, I will give you a few examples:

  • most of the components use NGRX Store to dispatch actions or gather data to show it in the template. So developers create components using the ng g c command and manually inject the Store class into a component constructor.
  • almost every component rely on Observables, and therefore it needs to manage subscriptions created within. How do developers usually go about this? They have to do a private field that will gather all subscriptions together, add another interface that the class implements - OnDestroy, create a method ngOnDestroy, and finally close all subscriptions in the ngOnDestroy method.
  • services are used for REST communication with API, so they have to have an injected HttpClient. I guess you already know what developers have to do after every service creation.

Doesn’t that sound to you like a bit of wasting a developer's time, possibly introducing bugs and inconsistency between classes? Imagine a situation where one developer names HttpClient object as http and another developer likes httpClient more ( project will end up with at least two words for the same thing across the solution), not very readable.

Do I have to create everything on my own?
Link to this section

These examples are perfect for discussing the details of our topic. Schematics not only offers predefined commands but also gives us the possibility to design our own!

When I was thinking about implementing custom schematics, I was worried the most about - what if I don't want to create my schematics from scratch? The Angular ones are fine. I just need to extend it a little bit - add some custom code into them. Just like in the examples I presented you above. They are not about creating something new but instead extending the default configuration that Angular Schematics gives us.

Schematics have an answer for this problem - we can use existing schematics and easily override what we want without implementing everything by ourselves.

This article aims to focus on overriding well-known schematics, making them custom based on project needs.

What is the plan?
Link to this section

I want to go through the implementation of custom schematics that overrides the default ones. I will start from the very beginning - creating a library, implementing an actual schematic for the component, and finally, schematics configuration for the Angular project. Beware, it will not be an article about all Schematics foundations - I will instead focus on our particular problem. There are a couple of other great articles about the Schematics basics and the docs that are especially good and make learning Schematics very easy.

Let's dive into code!
Link to this section

The implementation below will address one of the examples I showed you earlier - my goal would be to design a schematic that will override the default component schematic with automatically generated code for managing rxjs subscriptions.

Creating the library
Link to this section

We will begin by creating a library for schematics. Firstly, we need to install schematics-cli:

<>Copy
npm install -g @angular-devkit/schematics-cli

Using the schematics-cli, we are now ready to create a schematics project:

<>Copy
schematics blank --name=subscription-component

The blank schematic command will create a project with configured typescript, package.json, and initial schematic. The structure of the project should look like this:

<>Copy
subscription-component/ src/ subscription-component/ index.ts index_spec.ts collection.json package.json tsconfig.json

Aside from standard files that you recognize for sure, there are two worth explaining briefly:

  • collections.json - this is the main file, in which are defined all schematics that this project will expose
  • subscription-component/index.ts - this is the main file of our schematic, it contains a factory function which Schematics will use for generating our component

If you take a closer look into the collection.json file, you will see it includes our schematic and points directly to the factory function from index.ts.

<>Copy
{ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "subscription-component": { "description": "A blank schematic.", "factory": "./subscription-component/index#subscriptionComponent" } } }
collection.json

Now, what's inside a factory function?

<>Copy
export function subscriptionComponent(_options: any): Rule { return (tree: Tree, _context: SchematicContext) => { return tree; }; }
index.ts

Let’s briefly understand the factory function’s critical elements, which we will later use during the implementation.

  • _options - an object that keeps all input data from a caller. We will use it to get additional inputs as well as the name of the component
  • Rule - it’s an object that defines the transformations of the Tree. All we need to know, for now, is that we will need to build that, and this will precisely define files generation with the rules applicable to them.

Let's now focus on implementing our custom component schematic.

Implementing component schematic
Link to this section

Let's start by considering what we will need from the user as input to a schematic script? For our basic implementation, the only two crucial parameters are the name of the component that we will generate and the directory path.

Schema
Link to this section

To define input parameters, we need to create an additional file called schema.json in the schematic directory (so inside /subscription-component). Schema can contain many practical fields for describing the behavior of the script. Here is a straightforward example for our needs:

<>Copy
{ "$schema": "http://json-schema.org/schema", "id": "SubscriptionComponentSchema", "type": "object", "properties": { "path": { "type": "string", "format": "path", "visible": false }, "name": { "type": "string", "$default": { "$source": "argv", "index": 0 } } }, "required": [ "name" ] }
schema.json

Before-mentioned Schema will provide us with the very same result as the default ng generate component schematic input. When someone runs in the terminal ng generate component shared-components/custom-dialog, it will set the property path to shared-components/ and name to custom-dialog. That's all we need. The Schema should be included in the schematic definition in the collection file by adding such field:

<>Copy
"schema": "./subscription-component/schema.json"
collection.json

Now we can focus on the factory’s actual implementation, so let's take a look at the index.ts file.

Factory
Link to this section

We want to implement a factory function to read input parameters and then use the default component generation factory to generate all files for the component, except the ones that we will override.

How does the file generation work without getting into details? For each file generated via Schematics, the factory function is provided with a template file. The template file contains a definition of how the file should be constructed.

So how will we use that? We will simply create a template file for the typescript component class and merge that with other files generated by default Schematics.

First step: format input parameters so they can be safely used to creating files:

<>Copy
_options.name = basename(_options.name); _options.path = normalize('/' + dirname((_options.path + '/' + _options.name)));
index.ts

In case you've forgotten _options is the argument of the factory that keeps all input parameters. The functions for formatting these parameters are all taken from @angular-devkit package.

Second step: create template source for typescript component class

<>Copy
const templateSource = apply( url('./files'), [ template(_options), move(normalize(_options.path)), ], );
index.ts

Directory ./files will contain the file with template definition. How does it work?

Schematics creates template sources based on the static files from provided directory and rules applied to them. The set of rules defines what to do with the Source. Our example creates a template using the _options that keep dynamic data and then move all sources to the path.

Third, the last step: merge schematic factories.

In the end, we need to return the Rule object. In our case, it will be a Rule created based on the default component generation Rule merged with our Source.

<>Copy
return chain([ externalSchematic('@schematics/angular', 'component', _options), mergeWith(templateSource, MergeStrategy.Overwrite), ]);

Function externalSchematic creates a Rule from any external schematics - in our case, the default Angular ones. Then uses mergeWith function to merge rules, using the appropriate merging strategy provided as the second argument. That creates for us the final Rule that will generate an entire component for us.

Factory function should finally look like this:

<>Copy
export function subscriptionComponent(_options: any): Rule { return (_tree: Tree, _context: SchematicContext) => { _options.name = basename(_options.name); _options.path = normalize('/' + dirname((_options.path + '/' + _options.name))); const templateSource = apply( url('./files'), [ template(_options), move(_options.path), ], ); return chain([ externalSchematic('@schematics/angular', 'component', _options), mergeWith(templateSource, MergeStrategy.Overwrite), ]); }; }

Now it's time to write our template!

Template
Link to this section

Writing a template is a relatively easy job because it should look the same as well known generated file, except for one difference - all dynamic content, for example, the name of a component, has to be written inside special tags (<%= , %>), to print the value.

Template files are stored inside /files directory, and their names should be written using a specific format to allow dynamic naming of the files. For our case, we want to create a typescript file that will be in the form component-name.component.ts, assuming the "component-name" is our name provided as an input. To fulfill that, we need to create a template file with the name: __name@dasherize__.component.ts. Double underscore separates the dynamic content from the plain string, and the dasherize is an Angular function that will make a "kebab-case" string from the name. We have to use the same approach for naming a directory, so we need to place a template file within a folder called __name@dasherize__.

Now, the actual template:

<>Copy
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-<%= dasherize(name) %>-component', templateUrl: './<%= dasherize(name) %>.component.html', styleUrls: ['./<%= dasherize(name) %>.component.scss'], }) export class <%= classify(name) %>Component implements OnInit { constructor() { } ngOnInit(): void { } }
files/__name@dasherize__/__name@dasherize__.component.ts

I think you recognize the pattern? It’s a well-known component class but with the use of dynamic strings inside the special tags.

For the files and the selector, I'm using the dasherize(name) function to make a kebab-case name. Keep in mind that this function is not provided for the template yet - we will need to add it. The class name uses classify(name) function that converts the name to be in upper camelCase format - we have to provide this one too. So, to sum up, we need to provide two functions and the name of a component.

Let's look at the template source creation function:

<>Copy
template(_options),
index.ts - template source

It's pretty simple. As you see, it already contains the name variable for us (inside _options object). That means we only need to provide a classify and dasherize functions. Let's do that!

<>Copy
template({ ..._options, classify: strings.classify, dasherize: strings.dasherize, }),
index.ts - template source

strings is the collection from the @angular-devkit package, so the only thing we need to do is expose items we want to use.

Now we can extend that simple template with our subscription management code. That is our goal, right? There are a couple of ways to control subscriptions, and I will use the one with the Subscription field to keep all subscriptions and unsubscribe that on destroy hook.

That's how the template of the class looks like:

<>Copy
export class <%= classify(name) %>Component implements OnInit, OnDestroy { private readonly subscription: Subscription = new Subscription(); constructor() { } ngOnInit(): void { } ngOnDestroy(): void { this.subscription.unsubscribe(); } }
files/__name@dasherize__/__name@dasherize__.component.ts

Collection - final schematic configuration
Link to this section

I want to override the default ng g c / ng generate component schematic, so inside collection.json, I have to modify the name to be “component” and add the alias “c”.

<>Copy
"component": { "aliases": [ "c" ], "factory": "./subscription-component/index#subscriptionComponent", "schema": "./subscription-component/schema.json" }
collection.json

We are running a schematic!
Link to this section

There are a couple of ways to test schematics and its output. For this article’s purpose, I will use the easiest way to add custom schematics to the existing Angular project to run schematics without further tests.

We need to link our schematics directory to the Angular project and set it as the default schematics collection to override @angular/schematics.

First things first, we need to build our library with a schematic. Use the command below in the library directory:

<>Copy
npm run build

To add our schematics project as a dependency for Angular run in the project directory:

<>Copy
npm install --save-dev ../path/to/subscription-component

To override the default collection, you need to add it in the angular.json file as a cli property in the main object.

<>Copy
"cli": { "defaultCollection": "subscription-component" }
angular.json

Then in the same file, we need to provide default options, in the same way they are supplied for @angular/schematics:

<>Copy
"schematics": { "@schematics/angular:component": { "style": "scss" }, "subscription-component:component": { "style": "scss" } },
angular.json

Ready?
Link to this section

No more configuration, I promise. Now you should be able to run your custom schematic to generate a component with project personalized content!

<>Copy
ng g c app/my-component

The result should look like this, and you should be a proud owner of a brand-new component!

<>Copy
CREATE src/app/my-component/my-component.component.scss (0 bytes) CREATE src/app/my-component/my-component.component.html (21 bytes) CREATE src/app/my-component/my-component.component.spec.ts (626 bytes) CREATE src/app/my-component/my-component.component.ts (498 bytes) UPDATE src/app/app.module.ts (470 bytes)

Great job! You did it! Check the component.ts file for the subscription management code.

Improvements
Link to this section

There are many ways to improve our basic schematic, I will shortly describe some ideas later, but I want to change one more thing to make our schematic more useful in daily development.

I assume that although it could be the case that creating components with subscription handling is pretty common, it won't be useful in every case. I want to improve the schematic to take an additional parameter that will create a component with or without the subscription handing. Let's say it will be by default turned to true, but we will give the ability to disable the subscription control code.

Schema.json update
Link to this section

I will add additional parameter definition:

<>Copy
"subscriptionManagement": { "description": "Include subscription management code in the component class", "type": "boolean", "default": true, "alias": "subscription" }
schema.json

Template
Link to this section

Now inside the template file, we will use something called conditional templates. This technique will generate various fragments of a template based on the dynamic data passed in _options. The options object already contains the subscriptionManagement flag because it is an input parameter, so we don’t need to change the factory.

Conditional templates use the similar syntax as discussed in previous steps. The keywords we can use for building conditions are if and else. So in our case, we can implement it like this:

<>Copy
export class <%= classify(name) %>Component implements OnInit <% if (subscriptionManagement) {%>, OnDestroy <% }%> { <% if (subscriptionManagement) {%> private readonly subscription: Subscription = new Subscription(); <% }%> constructor() { } ngOnInit(): void { } <% if (subscriptionManagement) {%> ngOnDestroy(): void { this.subscription.unsubscribe(); } <% }%> }
files/__name@dasherize__/__name@dasherize__.component.ts

That's all! Now build the schematic, install it and check how it works.

<>Copy
ng g c app/my-component
should generate a component as before
<>Copy
ng g c app/my-component --subscription=false
should generate component without additional subscription code

Extra improvements
Link to this section

I mentioned above that there are a couple of things that you could improve when working on your schematics. Here is the list:

  • use the NPM registry to publish your schematics. It will make it easier to add your schematic for other developers
  • provide ng add support for your library. Remember when we had to set the default collection in angular.json manually? It could be all done automatically if you make another schematic for the ng add command
  • use migration schematics to help developers when introducing some breaking changes
  • add unit tests! I guess I don't need to say why
  • extend factory function to use default angular project options, like styles, selector prefix, etc.

Summary
Link to this section

I hope you found this article interesting and you already have some ideas about how you could apply it in your project to enhance the development process. I firmly encourage you to do it! It shouldn't take much effort - begin with some minimal value concept, include in schematic one of the critical snippets that is copied over and over, and by time add more.

To remind you briefly what exactly we have done, here is a short list of steps to implement a custom schematic that overrides the default angular one:

  1. create a blank schematic using the built-in command
  2. implement schematic
    -  factory function
    -  template file
    -  schema with input parameters definitions
    -  collection, which defines exposed schematics
  3. add schematic to Angular project

It's not a long process, so if you feel it could be useful for you, give it a go! You won’t be disappointed.

If you feel that you are missing some implementation details (which I hope you won't), here is a link to the Github repository with a working example from this article.

That's all. Thanks for staying with me!

Enjoy spending less time on copy and paste and more on an actual development! This is the way.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

author_image

About the author

Maciej Wojcik

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

About the author

author_image

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

Featured articles