Customizing Angle Schematics

Have you found yourself duplicating the same code snippets across many files? Perhaps you routinely add a ngOnDestroy method to handle subscription cleanup, or you keep injecting HttpClient into every service. If so, you are definitely not alone in this habit.

To be clear, there is nothing inherently wrong with repeating such patterns. This is not an article championing DRY principles or advocating for elaborate object-oriented hierarchies to eliminate duplication. Often, introducing complex structures solely to avoid repeated code is overkill; clear and straightforward classes matter more. This is precisely where Angular Schematics prove their worth.

Understanding Schematics

Chances are, you’ve already used Angular Schematics, perhaps without realizing it, when generating components or services. If the tool is unfamiliar, here’s a concise introduction:

Angular Schematics provides a range of commands for generating files, conducting migrations, and modifying existing code. To illustrate, running ng generate component my-component (or the shorthand ng g c my-component) creates the component directory along with the template, style file, and class file. Schematics handles all this automatically, generating three files with starter code. Quite convenient, isn't it?

The Case for Customization

What if your project consistently requires a specific configuration in generated files? Let me present a few typical scenarios:

  • Components often communicate with an NGRX Store, dispatching actions or selecting data. Developers typically generate a component with ng g c and then manually add the Store injection to its constructor.
  • Since many components rely on Observables, managing subscriptions is a common task. This usually means declaring a private field to accumulate subscriptions, implementing the OnDestroy interface, adding an ngOnDestroy method, and then unsubscribing within it.
  • Services frequently handle REST API communication, so injecting HttpClient is a near-universal requirement. Again, this means common manual post-generation steps.

Doesn’t this repetitive work seem like a drain on developer time, potentially introducing bugs and inconsistency? Consider a team where one developer names their HttpClient instance http while another prefers httpClient. Such discrepancies can lead to a codebase with multiple naming conventions for identical concepts, harming readability.

From Scratch or Extend?

These examples illustrate the core challenge perfectly. Schematics isn’t just about predefined commands; it allows you to design your own generators.

A primary worry for me was the possibility of needing to build a schematic system entirely from zero. The default Angular ones are quite decent; I only wished to enhance them. My goal wasn’t to create new, unrelated generators but to augment the standard configuration, just like in the scenarios above—extending, not replacing, the defaults.

Fortunately, Schematics has a solution. You can leverage the existing schematics, overriding specific parts to suit your needs, without implementing everything from the ground up.

This article concentrates on overriding standard schematics to tailor them to your project's specific requirements.

Our Strategy

We will work through the details of creating custom schematics that override the default ones. We’ll start with the fundamentals: creating a schematic library, building a custom component schematic, and then wiring up its configuration for use in an Angular project. Please note that this isn't a complete guide to all Schematics concepts; we'll focus specifically on the problem of overriding. For a deeper dive into the basics, the official documentation and other tutorials are excellent resources.

Getting Started with Implementation

The upcoming implementation will target one of the earlier examples. Our aim is to design a schematic that overrides the default component generator, automatically incorporating code for managing rxjs subscriptions.

Setting Up the Library

First, we need to set up a new library project for our schematics. We start by installing _schematics-cli_:

npm install -g @angular-devkit/schematics-cli

With schematics-cli available, we can scaffold a new schematics project:

schematics blank --name=subscription-component

The "blank" schematic command creates a project with configured TypeScript, a package.json, and an initial schematic. After creation, the project structure should look like this:

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

Beyond the familiar standard files, two files are particularly important to understand:

  • collections.json – This is the project’s manifest, defining all the schematics that will be exposed by this library.
  • subscription-component/index.ts – This is the entry point for our schematic. It exports a factory function that Schematics will call to generate our component.

Looking inside the collection.json file, you'll see it lists our schematic and points to the factory function in index.ts.

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

collection.json

Now, Let’s examine the factory function’s contents:

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

index.ts

Let’s clarify the purpose of the factory function's key elements, which we will use later on.

  • _options – This object contains all the input data provided by the caller. We will use it to retrieve the component's name and any other additional options.
  • Rule – This is the core concept that dictates the transformations to apply to the file tree. For now, it's sufficient to know we will construct this object to define how our files should be created and modified.

With this understanding, we can now focus on implementing our custom component schematic.

Putting the component schematic together

Before writing the factory function, let's decide what inputs the schematic should accept. For the core use case, a component name and a destination directory are the only pieces of information we truly need.

Defining the schema

Input parameters are declared in a schema.json file placed inside the schematic folder (i.e., within /subscription-component). This schema supports a wide range of fields that dictate how the script behaves. A minimal example is shown below:

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

This schema mirrors the input structure of the standard ng generate component command. Running ng generate component shared-components/custom-dialog populates the path property with shared-components/ and the name property with custom-dialog. That's sufficient for our purposes. The schema must also be registered in the collection file by adding the following entry:

"schema": "./subscription-component/schema.json"

collection.json

With the schema in place, the real work begins in the factory function located in index.ts.

The factory function

The goal is to read the input parameters, invoke the default component factory to produce the standard files, and then override a few of them with our own implementations.

To understand file generation at a high level: each generated file corresponds to a template file provided to the factory. The template dictates the file's final content.

Our approach is straightforward: we create a template for the TypeScript component class and combine it with the other files generated by the default schematic.

First step: sanitize the input parameters for safe file creation.

_options.name = basename(_options.name);
_options.path = normalize('/' + dirname((_options.path + '/' + _options.name)));

index.ts

Recall that _options is the factory's argument containing all user-supplied parameters. The utility functions used for formatting come from the @angular-devkit package.

Second step: prepare the template source for the TypeScript component class.

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

index.ts

The ./files directory holds the template definitions. How does this work?

Schematics builds template sources from static files in a directory, subject to a set of rules. These rules dictate how the source is processed. Our example applies the _options containing dynamic data to the template and then moves the resulting files to the appropriate destination.

Third, the final step: integrate the schematic factories.

The last requirement is to return a Rule. In our scenario, this is a combination of the default component generation rule and our custom source.

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

The externalSchematic function creates a Rule from any external schematic—here, the standard Angular ones. The mergeWith function then combines the rules, using a merging strategy passed as its second argument. This produces the final Rule that generates the entire component.

Here's the complete factory function:

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, let's create the template!

Crafting the template

Templates resemble familiar generated files, but dynamic content—like component names—is wrapped in special tags (<%= and %>) to print the value.

Templates reside in the /files directory and their filenames follow a specific convention to enable dynamic naming. For a file like component-name.component.ts, we create a template named __name@dasherize__.component.ts. Double underscores enclose the dynamic segment, and dasherize is an Angular function that converts the name to "kebab-case". The same applies to directory names, so the template file is nested inside a folder called __name@dasherize__.

Here’s the actual template:

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

The pattern is recognizable—a standard component class with dynamic values plugged in via the special tags.

For file names and the selector, I rely on dasherize(name) to produce a kebab-case version. Note that this function won't be automatically available in the template; we need to supply it ourselves. The class name uses classify(name), which converts the input to upper camelCase—this also requires explicit provision. So, we need to make two functions and the component name available to the template.

Here’s how we set up the template source:

template(_options),

index.ts – template source

It's quite simple. As you can see, the name variable is already provided in the _options object. Thus, we only need to add the classify and dasherize functions. Let's do exactly that.

template({
	..._options,
	classify: strings.classify,
	dasherize: strings.dasherize,
}),

index.ts – template source

The strings collection comes from the @angular-devkit package, so exposing the needed functions is straightforward.

We can now expand this basic template to include our subscription management logic—the core reason for this exercise. There are multiple ways to handle subscriptions; I'll use a Subscription field to track them all and unsubscribe in the destroy hook.

The class template looks like this:

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

Final schematic configuration

To intercept the ng g c/ng generate component command, we need to rename our schematic to "component" and assign it the alias "c" in the collection.json file.

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

collection.json

Let's run it

Testing schematics can take various forms. For this article, the simplest path is linking our custom schematics to an Angular project and setting it as the default collection, thus replacing @angular/schematics.

First, we must compile our library. Execute this in the library directory:

npm run build

To register the schematics project as a dependency, run this in the Angular project directory:

npm install --save-dev ../path/to/subscription-component

To override the default collection, add it as a cli property in the main object of angular.json.

"cli": {
	"defaultCollection": "subscription-component"
}

angular.json

Next, we provide default options at the same level, just as with @angular/schematics:

"schematics": {
	"@schematics/angular:component": {
		"style": "scss"
	},
	"subscription-component:component": {
		"style": "scss"
	}
},

angular.json

All set?

Configuration is complete—promise! You can now run your custom schematic to produce a component enriched with your project's specific content.

ng g c app/my-component

The output should resemble the following, and you'll be the proud owner of a new component.

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! Check the component.ts file to see the subscription management code in action.

Potential enhancements

Our basic schematic can evolve in several directions—ideas are listed below—but I'd like to make one final adjustment to boost its everyday utility.

While generating components with subscription handling is useful, it's not universally applicable. I'll refine the schematic to accept an extra parameter that toggles the subscription code on or off. By default, it will be enabled, but users can opt out.

Updating the schema

We add a new field to the schema definition:

"subscriptionManagement": {
	"description": "Include subscription management code in the component class",
	"type": "boolean",
	"default": true,
	"alias": "subscription"
}

schema.json

Template adjustments

Within the template, we'll leverage conditional templates. This feature generates different template fragments based on the dynamic data in _options. Since subscriptionManagement is an input parameter, the options object already contains this flag, so altering the factory isn't necessary.

Conditional templates use a familiar syntax, with the keywords if and else for branching. Our implementation looks like this:

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 it! Rebuild the schematic, link it, and verify the behavior.

ng g c app/my-component

should generate a component as before

ng g c app/my-component --subscription=false

should generate component without additional subscription code

Further improvement ideas

Several enhancements could make your schematic even more robust. Here's a curated list:

  • publish your schematic to the NPM registry to simplify adoption for other developers
  • implement ng add support. Manually setting the default collection in angular.json can be automated with a dedicated schematic for the ng add command
  • create migration schematics to ease developer transitions during breaking changes
  • write unit tests—the reasons are self-evident
  • extend the factory to incorporate default Angular project options such as styles and selector prefixes

Wrapping up

I hope this deep dive sparks ideas for integrating schematics into your projects. I strongly recommend giving it a shot! The effort is minimal—start with a small, valuable snippet you copy frequently and gradually expand.

For a quick recap, here are the steps to implement a custom schematic that supersedes the default Angular one:

  1. generate a blank schematic with the built-in command
  2. implement the schematic parts
    – factory function
    – template file
    – schema for input parameters
    – collection to expose your schematics
  3. link the schematic into your Angular project

The process is quick, so if you see value, jump right in—you won't regret it.

If any implementation details feel unclear (I hope not), a complete working example is available on GitHub.

That wraps it up. Thanks for reading!

Enjoy spending less time on copy-paste and more on real development. This is the way.