Angular Component library: The Definitive Guide
There’s plenty of articles around the web about creating Angular components and libraries so what’s the point in creating another one?
Well, after having followed almost any publicly available source of information about this topic around the web, I came to the conclusion that they are all uncompleted and superficials so I’ve never been able to create my production-ready library following their article. Then I decided to create this post, hoping to help future developer struggling with the same problematics I encountered.
Angular CLI is for sure one of the best tools available for building Angular applications. It’s a powerful tool when it comes to creating applications but is not really helpful when you need to create a reusable module or even worse a component library.
Good news! The CLI team is aware of this and is already working on an official solution for developing component library using Angular CLI. In the meantime, however, we need a plan-B.
My solution requires a combination of 2 independent Angular CLI projects and a lib
folder containing my distribution-ready component library.
Project structure
What we need in our project, as already mentioned before, is 1 Angular-CLI project for developing our library, 1 folder containing the library itself and 1 extra Angular CLI project for 2 reasons: Creating a documentation app and testing our library inside an external application for simulating a real implementation of our library.
Our goal is in fact to release a library ready to be installed inside any Angular application so, the only way we have for making sure our library is serving that nrwl.io is installing it inside an external Angular application and making sure is working with both JiT and AoT compilation.
The following screenshot represent my ideal library project structure.
The root folder is where I bootstrapped my development Angular-CLI application and my library is living inside the source folder of it.
There is a second Angular-CLI application installed inside the doc folder but we can ignore that for now.
Why should I care about AoT?
From https://angular.io/guide/aot-compiler
Faster rendering
With AOT, the browser downloads a pre-compiled version of the application. The browser loads executable code so it can render the application immediately, without waiting to compile the app first.Fewer asynchronous requests
The compiler inlines external HTML templates and CSS style sheets within the application JavaScript, eliminating separate ajax requests for those source files.Smaller Angular framework download size
There’s no need to download the Angular compiler if the app is already compiled. The compiler is roughly half of Angular itself, so omitting it dramatically reduces the application payload.Detect template errors earlier
The AOT compiler detects and reports template binding errors during the build step before users can see them.Better security
AOT compiles HTML templates and components into JavaScript files long before they are served to the client. With no templates to read and no risky client-side HTML or JavaScript evaluation, there are fewer opportunities for injection attacks.
Before we start
Because we need to make sure our library is both AoT and JiT ready, there are a couple of thinks that needs to be clarified before writing any code. There is a good article from Isaac Mann available here . Highly recommend a good read.
This is what I believe are the most important points to remember:
Any member accessed from the template must be public
@Component({
template: `<div> {{ context }} </div>`
})
export class SomeComponent {
private context: any;
}
This will work with Just-in-Time compilation but will break AoT.
The private context variable needs to be changed public instead of private.
Export components explicitly
Error:
Uncaught Error: Unexpected value ‘undefined’ imported by the module ‘AppModule’
Before:
export * from 'some-component';
After:
export { SomeComponent } from 'some-component';
peerDependency
This is another good point as I noticed some open-source libraries don’t include any peerDependencies in their package.json
. This can lead to several problems when the library is installed in a project with different package versions.
Have a look at this NodeJS article for more information.
Starting from scratch
Create a new Angular-CLI project with ng new angular-component-library
assuming that you’ve already installed Angular CLI globally on your machine (If not, follow the official documentation).
Now, we have our Development app that will be used for developing and testing our component library.
Writing our library
First of all, we’re not going to create an Angular module but a library which is a collection of modules. This means that we’re going to have several independent modules sitting together inside the same box and ready to be used inside any Angular application.
This means that our lib
folder will just export all the different public folders for our library and containing one or more modules each.
Our lib file structure will be then something similar to this:
.
├── lib
| ├── module-a
| | ├── components
| | | ├── component-a
| | | | ├── component-a.component.html
| | | | ├── component-a.component.css
| | | | ├── component-a.component.ts
| | | | ├── component-a.component.spec.ts
| | | | └── index.ts
| | | └── index.ts
| | ├── module-a.module.ts
| | └── index.ts
| ├── module-b
| | └── ...
| └── index.ts
The index.ts
file sitting inside the root directory of lib
will be responsible for exporting all the public modules in our project and will be something similar to this:
export * from './module-a/index';
export * from './module-b/index';
export * from './module-c/index';
export * from './shared/index';
Then, the main index.ts
files sitting inside each module folders will export all the different elements of the module itself:
// lib/module-a/index.ts
export { ComponentAService } from './services/index';
export { ComponentAComponent } from './components/index';
export { ComponentAModule } from './component-a.module';
Noticed all those /index
? Well, it turns out that we need to specify them when we want to keep advantage of barrels where using export
keyword. If missing, the type definition will be compromised during the library packaging process using the Typescript compiler (AKA tsc
). There are several discussions and issues reported about this topic around the web and it took me a long while to figure it out.
I can share this issue reported on the Typescript repository as an example: https://github.com/Microsoft/TypeScript/issues/12974
Library development
Now we can start writing our components library and see them in action using the Angular-CLI development project so we can benefit from the scaffolding tool for generating our modules, components, etc and having a live reload listening to both the development app and our library which is sitting inside.
Module Resolution
It would be great if we can refer, from inside our development application, to our library like it was a real package sitting inside our node_module
folder. So let’s do that!
From the Typescript documentation:
The TypeScript compiler supports the declaration of such mappings using
"paths"
property intsconfig.json
files. Here is an example for how to specify the"paths"
property forjquery
.
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
}
}
}
For more information, refer to the official documentation available at: https://www.typescriptlang.org/docs/handbook/module-resolution.html
Using the typescript configuration file, we can create a map to our library to be used inside our development app without the need of specifying its relative path.
So, let’s edit the tsconfig.app.json
file located inside the src
folder of our application, and add the following “paths” node:
{
"compilerOptions": {
...
"paths": {
"@my/lib": ["lib/public_api.ts"]
}
}
}
Note that we don’t need to include the same paths inside the tsconfig.spec.ts
as we’re not going to have unit test inside the development application, instead, all the spec files will be only sitting inside the lib
folder.
Now we can simply refer to @my/lib
inside our development application like the following example:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RoutingModule } from './accordion.routing';
import { AccordionPageComponent } from './accordion.component';
import { AccordionModule } from '@my/lib'; // '@my/lib' can be replaced with '../../../lib'@NgModule({
declarations: [AccordionPageComponent],imports: [
CommonModule,
AccordionModule,
RoutingModule
]
})
export class PageModule { }
Packaging
For packaging my library, I’m going to use ng-packgr. This is a great tool for generating production-ready Angular library with zero (or very close to) configuration. Ng-packgr follows the Angular Package Format guideline and the best practices suggested by Jason Aden (Library developer at Google). Watch this great video from NG-Cong 2017 about Packaging Angular.
ng-packgr needs 1 configuration file and 1 npm dependency.
npm i -D ng-packagr
Then update your package.json file with a package script:
{
"scripts": {
"package": "ng-packagr -p src/lib/ng-package.json"
}
}
The configuration is picked up from the cli -p
parameter, then from the ng-package.json
.
Inside the src/lib/ng-package.json
we need to write the following configuration:
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../package",
"lib": {
"entryFile": "public_api.ts"
}
}
The default entry-file is set to public_api.ts
. We can either create a public_api.ts
file inside our lib
folder or set index.ts
to be our new entry-file.
We also need a package.json
for our library. We can theoretically recycle the one sitting inside our root folder but is full of unnecessary packages because contains all the dependencies for our sandbox application. When we deliver an npm package we just want that to rely its own dependencies and not to carry over unwanted packages. Also, because we’re going to release an Angular library, we’ll need to specify some peer dependencies to avoid conflicts.
Then, the best way for having a clean package.json
for our library is to create a brand new one inside our lib
folder to be used for the distribution version of the library itself.
I created a very basic bash script in my root folder to dynamically replace some placeholders in a package.tpl.json
file inside src/lib/
directory. In this way I can have the package name and version automatically synchronised with my main package.json file without the need of replacing the value twice every time I release a newer version of my library, then I created a prepackage script to be launched by npm to execute my script.
package.json
{
"scripts": {
"prepackage": "./predeploy.sh"
}
}
predeploy.sh
#!/bin/bashPACKAGE_VERSION=$(grep -m1 version package.json | awk -F: '{ print $2 }' | sed 's/[", ]//g')PACKAGE_NAME=$(grep -m1 name package.json | awk -F: '{ print $2 }' | sed 's/[", ]//g')sed -e "s/{version}/${PACKAGE_VERSION}/" -e "s#{pkg-name}#${PACKAGE_NAME}#" src/lib/package.tpl.json > src/lib/package.json
src/lib/package.tmp.json
{
"name": "{pkg-name}",
"version": "{version}",
"peerDependencies": {
"@angular/core": "^4.0.0 || ^5.0.0",
"@angular/common": "^4.0.0 || ^5.0.0"
},
"dependencies": {
"@angular/cdk": "^2.0.0-beta.12",
}
}
Now, running npm run package
will generate a package
folder containing your library’s package.json with the correct dependencies.
Documentation time
Now that our library is ready for “going live” we can talk about that doc
folder I mentioned before.
I said that we’re going to use another Angular-CLI project for both testing our library in action and providing a library documentation.
Inside my doc
folder I bootstrapped and Angular-CLI project and removed all the testing tools and logic, which means that I deleted the e2e folder and all the karma and protractor code related to the documentation application as our library has already been unit and e2e tested from inside the src
folder. The purpose of this project is to provide a documentation to our library so it doesn’t need to be tested but it will act as acceptance test for our library to make sure the final users will be able to consume our creation inside their projects.
For doing this, there are 2 possible ways (actually 3 because you can also create a symlink of the package but that can lead to other unwanted side-effects so I’m not going to talk about that in this article).
- Deploying the generated library to an npm repository and installing it inside our doc application.
- The second, and the one I prefer, is to package the generated distribution version of the library and install that locally inside the doc application using npm.
We can use npm pack
inside our generated distribution version of the package, for generating a tarball file ready to be installed with npm like a normal npm package using npm install
.
I defined a doc script inside my package.json to automate that process and serve the doc application with my latest version of the library injected inside.
"scripts": {
"doc:clean": "rimraf doc/package-lock.json doc/node_modules",
"prepack-lib": "npm run package",
"pack-lib": "cd package && npm pack",
"predoc:serve": "npm run pack-lib && npm run doc:clean",
"doc:serve": "cd doc && npm i && npm i ../package/*.tgz --no-save && npm start"
}
What I’m doing here is generating a fresh new distribution-ready version of my library, packaging everything and installing the generated tgz
file inside my doc app. Note that I specify --no-save
, that is because I don’t want to update the doc’s package.json dependencies with my library as I want to manually install that every time I run the documentation project to make sure I’m always using the latest version of the generated library.
I’m also using rimraf for cleaning all the node modules installed and the package-lock.json
file that Node >8 generates automatically. I’m using rimraf installed as a devDependenciy inside my project instead of rm -rf
because it automatically deletes all files and folders recursively and doesn’t complain if the target file doesn’t exist.
Automated documentation
As an extra, I’m using Compodoc on top of my documentation app to automatically generates an API documentation for my library using the power of the type definition. A good documentation example using a combination of Angular and Compodoc is available here.
What I’m doing is installing Compodoc as a development dependency of my project and generegate the documentation project inside my doc folder.
"predoc:build": "cd doc && npm i && npm i ../package/*.tgz --no-save && npm build --prod",
"doc:build": "compodoc -p src/lib/tsconfig-compodoc.json -d doc/docs"
The tsconfig-compodoc.json
is required for specifying the files you want to use for generating the documentation.
I’m also using a different tsconfig
file as I don’t want to document my development application but only the library sitting inside.
That’s all folks!
I really hope this guide will guide you in your library development process and will answer all your questions about that process.