Implementing Angular Schematics using Angular + Tailwind CSS example
In this article, I am going to start with very basic Angular schematic implementation and slowly build the code for this complete angular-tailwindcss-schematics project. At each milestone, I will share the git commit link to check the code and files at that stage and also explaining the step.

This article requires that you understand the basic terminologies and concepts used in Angular Schematics such asCollection
,Rule
,Tree
, etc. If you want to learn more about it, check here and here.
Recently, I thought of giving tailwindcss a try (after hearing and reading so much about this utility-first CSS framework). My plan was to use tailwindcss
with Angular. So I started looking for documents on how to integrate tailwindcss
with Angular and found couple of good articles. Check them here and here.
Going through these document, you will soon understand that integrating tailwindcss
to Angular is not that straightforward (as in executing a single command) but involves a series of steps.
Luckily I also found this library that makes the integration really simple (yes, executing a single command!). This library uses Angular schematics to automate this process.
Now just to understand better, first I followed the manual process and then also tried the schematics library (@ngneat/tailwind
) to integrate tailwindcss
to Angular. My primary goal was completed.
However, few months back, I had started learning about Schematics in Angular and like many other learning and side projects, I had abandoned it in favor of something else (do not remember exactly what for). I thought of resuming it back and this time not only learn the concepts but actually create a useful (but simple) Angular schematic.
As mentioned before in this article, I will not be going into explaining concepts related to Angular Schematics. Please refer this and this for detailed explanation. Going through these articles and reading more about Schematics, I would say
Schematics are all about code generation and changing of existing files!
and the main files that we will work with are:
collection.json
- schematic definition file.index.ts
- Schematic factory function file.schema.json
- Contains options to be passed to the factory function.schema.ts
- Contains interface for the schematic optionsfiles
- Directory that contains template file to be created.
With this understanding, I realized that creating a schematic for Angular + Tailwind CSS
integration would be a good beginner learning experience for following reasons.
- It involves code generation (Adding tailwindcss imports, etc.).
- It involves updating existing files. (angular.json, etc)
- It involves creating new files (webpack config, etc.).
- It involves updating
package.json
file and installing libraries.
More or less it covers all the things that Angular schematics is designed to provide. Also I had couple of good blogs and an already implemented schematic library for my reference.
Thank you to all the wonderful people who have written about this topic and contributed to the development of libraries that uses Angular schematic.
So with the goal of learning and understanding the Angular Schematics and also working on a schematic library I jumped right on my next project - angular-tailwindcss-schematics
.
In the next section, I am going to share my git commit
timeline for this project and for each share, I am going to showcase/explain these things:
- What's the step is about (What it does)?
- Link to the generated code, related files and explain things (if required)
- How it is related to Angular Schematics?
Plan is to start with very basic schematic concepts, implementation and slowly build the code for this complete angular-tailwindcss-schematics
project.
If you want to take a look at code on Github → Click here
Click here to check this simple Angular Schematic on npm → ngx-tailwindcss-schematic
Setup
First things first, I created a git
repository called angular-tailwindcss-schematics
and cloned it on my local machine.
We have to install @angular-devkit/schematics-cli
package to be able to to use schematics
command in our terminal. This enables us to create a blank schematics project.
npm i -g @angular-devkit/schematics-cli
Create Project
Run: schematics blank angular-tailwindcss-schematics
- This command creates a new schematic project mainly used to create an standalone angular schematic that can be run in any Angular Cli application.
- Following files are generated:
CREATE angular-tailwindcss-schematics/README.md (639 bytes)
CREATE angular-tailwindcss-schematics/.gitignore (191 bytes)
CREATE angular-tailwindcss-schematics/.npmignore (64 bytes)
CREATE angular-tailwindcss-schematics/package.json (587 bytes)
CREATE angular-tailwindcss-schematics/tsconfig.json (656 bytes)
CREATE angular-tailwindcss-schematics/src/collection.json (284 bytes)
CREATE angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index.ts (335 bytes)
CREATE angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index_spec.ts (539 bytes)
✔ Packages installed successfully.
Among all files generated, we have two files that I want to mention about:
collection.json
- This file contains information such as schematic name, description and path to our schema factory method.angular-tailwindcss-schematics/src/angular-tailwindcss-schematics/index.ts
- This is schema factory file. It contains a factory function that returns aRule
. We will be writing most of the code here.
Git commit at this stage: Code
Rename to ng-add
In this commit, I renamed angular-tailwindcss-schematics
to ng-add
following standards as this will be going a schematic that you will add to you app.
Made the factory function in index.ts
as default
and accordingly made changes in the collection.json
file.
Git commit at this stage: Code
Added Schema file
schema.json
file is another important file that is used to provide additional schema options while running your schematic. It also adds a validation layer. We can also add schema.ts
interface file for type safety with list of options that we want to provide.
Our default factory function in file index.ts
, receives options:Schema
as parameter. This options
object contains schema options from command line.
Another update that I did was to add a reference to this newly created schema.json
file in our collection.json
file.
Git commit at this stage: Code
Added files directory
In this commit, I created a files
directory (src/ng-add/files
). This is the location to store template files that can be used to generate files using the schematic.
You will notice usage of words such as dasherize
, classify
, etc. These are the helper functions provided by the schematic library (strings
module) to transform names and are commonly used for templating purposes. Read more about them here.
I have updated index.ts
file with basic code to generate a file based on the template from files
directory. Added comments to help understand the code.
Git commit at this stage: Code
Building/Running Schematics
At this point, we have all the required building blocks to work on a schematic, though it does not do anything useful (yet!). You can build
and test
the schematic.
// Build
npm run build
// OR run in watch mode
npm run build:watch
// Run
// dry-run mode
schematics .:ng-add
// normal mode
schematics .:ng-add --debug false
At this point, we have a basic working Angular Schematic and now we will update/change these already generated files/code to create a schematic to add tailwindcss
to any Angular Cli application or Nx workspace.
To add tailwindcss
, we need to do following changes in our workspace:
- Add
tailwindcss
dependencies topackage.json
file and install them.
tailwindcss
postcss-import
postcss-loader
@angular-builders/custom-webpack
postcss-scss (only needed if selected style type is scss)
- Update Project default styles file with
tailwindcss imports
.
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
- Create following config files:
- tailwind.config.js
- webpack.config.js
- Update
angular.json
file with custom angular webpack builder.
Changes to index.ts
file.
Before making above changes, I am going to briefly describe the changes I have done to default function in index.ts
file.
// ng-add/index.ts (entry point for schematic execution)
import { chain, 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.
/** Rule factory: returns a rule (function) */
export default function (options: Schema): Rule {
// this is a rule. It takes a `tree`, apply changes and returns
// updated tree for further processing by next rule.
// this way the schematic rules are composable.
return (tree: Tree, context: SchematicContext) => {
// Read `angular.json` as buffer
const workspaceConfigBuffer = tree.read('angular.json');
if (!workspaceConfigBuffer) {
throw new SchematicsException('Could not find an Angular workspace configuration'); ? (Make sure you are in Angular Cli workspace)
}
// parse config only when not null
const workspaceConfig: workspace.WorkspaceSchema = JSON.parse(workspaceConfigBuffer.toString());
// if project is not passed (--project), use default project name
if (!options.project && workspaceConfig.defaultProject) {
options.project = workspaceConfig.defaultProject;
}
const projectName = options.project as string;
// elect project from projects array in `angular.json` file
const project: workspace.WorkspaceProject = workspaceConfig.projects[projectName];
if (!project) {
throw new SchematicsException(`Project ${projectName} is not defined in this workspace.`);
}
// compose all rules using chain Rule.
return chain([])(tree, context); ?
};
}
Our default factory function in index.ts
file is the only exportable function in that file and is referenced in the collection.json
.
In above code example, when you execute this schematic, I have added code to make sure that you are in a Angular Cli workspace and that we have a valid project. If any one fails, an exception is thrown
.
Another important addition is chain
Rule function. It returns a Rule
by combining several other Rule
's. Read more about it here. So basically for each of the task for tailwindcss
we will be creating a function that will return a Rule
and then we can call those function from this chain
function as shown below:
// Example
// ng-add/index.ts
export default function (options: Schema): Rule {
return (tree: Tree, context: SchematicContext) => {
...
...
// compose all rules using chain Rule.
return chain([addDependencies(options)])(tree, context);
}
}
/**
* Add required dependencies to package.json file.
* @private
*/
function addDependencies(options: Schema): Rule {
...
...
}
So now our skeleton code is ready and we only have to perform 2 tasks:
- Add a function (for any
tailwindcss
related tasks). - Update
chain([])
function.
There are couple of ways to build and test the schematic from an Angular Cli workspace. Check the ReadMe
Let's do these changes one by one.
Add a function to update package.json
file
We have seen above, the list of packages that is required as dependencies for tailwindcss
and it also depends on CSS
types or preprocessors (css, scss, sass, less). So in this commit the primary task was to:
- Give user's option to select default CSS type for the project.
- Take user selection and pass it to a method that will add the packages to
package.json
file.
For point 1, We need to update the schema.json
to add the prompt that will provide with the CSS
options. The schema.ts
file also needs to be updated. Check code changes below
Git commit at this stage: Code
For point 2, I have added addDependencies(options: Schema): Rule
method. This method takes options
(it has all the custom options, like cssType, etc.) and updates the package.json
file with the dependencies required for tailwindcss
. They are added as Devdependencies
.
If the default project style is notcss
then we needed to add an extra dependencypostcss-${options.cssType}
.
The method is pretty much self explanatory and it uses couple of helper functions from @schematics/angular/utility/dependencies
Git commit at this stage: Code
The last thing was to update the chain([addDependencies(options)])
function in index.ts
file.
Add a function to add config files for tailwindcss
As we have seen above, we need to add config files for tailwind
and webpack
when we run our schematic.
So in this commit, I have added the addTemplateFiles(options: Schema): Rule
that goes through the files
directory and these files to your project.
Git commit at this stage: Code
Similar to previous step, update the chain([addDependencies(options), addTemplateFiles(options)])
with this new method.
Add a function to add tailwindcss imports to default project style file
We need to update the project default style file (for e.g. styles.css or styles.scss) with the imports
from tailwindcss
. Below helper method gives us the import string.
export function getTailwindCSSImports(): string {
return `
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`;
}
Now in order to complete this requirement, we should know the default styles file for the project.
angular.json
file has all these information, to be more specific, the architect[buildTarget].options
object in this file has the styles
array with default project style file path. I will use this to get the required information.
// angular.json
// architect['build'].options
...
...
"architect": {
"build": { ? // buildTarget: 'build'
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/sample-app",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss" ? // default style file
],
"scripts": []
},
...
...
...
...
So, I have added getProjectDefaultStyleFile(project: workspace.WorkspaceProject, fileExtension: string)
helper method to return the the style path.
It takes the project
(basically part angular.json
file object) and css style file type (from the user input) and just iterate down the JSON object to get the path.
Git commit at this stage: Code
Now once we have the style path, we can go ahead and write our main method that will update the style file. I have added updateStylesFile(options: Schema, project: workspace.WorkspaceProject): Rule
that updates the style file based on the default style path.
Git commit at this stage: Code
Similar to previous step, update the chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options)])
with this new method.
Add a function to add update angular.json
file
Next we need to update the angular.json
file with custom webpack builder
and webpack config file
. This is required because tailwindcss
needs to be part of the build process (Read more here) so that it gets properly imported and we can take advantage of all the things it has to offer.
// Custom webpack builder
@angular-builders/custom-webpack
// webpack config file
webpack.config.js
Similar to previous task, we need to iterate through the JSON object in angular.json
and this time look for architect[buildTarget].builder
. We need to update the value for this key with the custom webpack builder and also update architect[buildTarget].options
object with custom webpack file.
I have added a helper function to get us the target based on the builder name. It takes the project
object and builderName
(browser, devServer, etc.) and returns us the respective target.
// File: src/ng-add/utils.ts
/** Gets all targets from the given project that match the specified builder name. */
export function getTargetsByBuilderName(project: workspace.WorkspaceProject, builderName: string) {
const targets = project.architect || {};
return Object.keys(targets)
.filter((name) => targets[name].builder === builderName)
.map((name) => targets[name]);
}
Once we have the respective target, we can update the builder
and options
object as required.
Git commit at this stage: Code
Similar to previous step, update the chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options), updateAngularJsonFile(workspaceConfig, project)])
with this new method.
Add a function to add install added dependencies
Last part is to add a method to install the dependencies that we had added in the packge.json
file for tailwindcss
. For this we will use builtin class NodePackageInstallTask
from '@angular-devkit/schematics/tasks'
.
Git commit at this stage: Code
and update the chain([addDependencies(options), updateStylesFile(options, project), addTemplateFiles(options), updateAngularJsonFile(workspaceConfig, project), install()])
with this new method.
The complete chain([])
method has all the methods that returns a Rule and it composes all the Rules one by one, passing along the updated tree
from one method to another.
/** Rule factory: returns a rule (function) */
export default function (options: Schema): Rule {
// this is a rule (function). It takes a `tree` and returns updated `tree`.
return (tree: Tree, context: SchematicContext) => {
...
...
// compose all rules using chain Rule.
return chain([
addDependencies(options),
updateStylesFile(options, project),
addTemplateFiles(options),
updateAngularJsonFile(workspaceConfig, project),
install(),
])(tree, context);
};
}
Again to run and build, you can refer ReadMe
- Here is the npm link of my published package —> ngx-tailwindcss-schematic
- Also, you can check the complete code on Github
I want to Thanks Angular team for doing such awesome job. Keep up the good work!
Do let me know in comments if you have any feedback/suggestions or you find any errors. You can find me on Twitter (@esanjiv).
Keep Learning! Thanks!