How to create an Angular 2 component library, and how to consume it using SystemJs or Webpack
In this post we are going to see how an Angular 2 component library can be built and then consumed. Check this repository for a minimal sample library and examples on how to consume it in both SystemJs and Webpack. Let’s go over the following topics:
- Choosing a format to publish an Angular 2 library – CommonJs
- Setting up the Typescript compiler
- Publish typings and source maps
- exposing the library public API
- How to publish component CSS
- Isolating CSS via component encapsulation
- Overriding CSS if needed
- publishing a library to npm
- Consuming a library using SystemJs
- Consuming a library using Webpack
Choosing a format to publish an Angular 2 library
During the alpha stage of Angular, the Angular bundles where initially published in SystemJs format, but now they are published in CommonJs (see here). This happened for a couple of good reasons:
- CommonJs is easily consumable by existing popular tools such as Browserify or Webpack
- CommonJs can also be easily consumed by SystemJs
- CommonJs is the node.js module format, which allows for components to be more simply used in server-side rendering
- the size of CommonJs bundles is smaller (in the case of Angular the gain was around 20%)
Due to the above-mentioned reasons, the current most convenient format to publish an Angular 2 library is a set of Javascript-only files in the CommonJs module format.
What does a published library look like?
Lets take a look at what the sample library (named angular2-library-example) looks like when published to npm:
. |____components.d.ts |____components.js |____lib | |____HelloWorld.d.ts | |____HelloWorld.js | |____HelloWorld.js.map |____package.json
The following are the different components of the library:
components.js
andcomponents.d.ts
define the public API of the library.- the lib folder contains the bulk of the library, in this case the Angular 2 component named
HelloWorld
- each Javascript file is published together with the Typescript type definitions and source maps
- each Javascript file follows the CommonJs module format of node, exporting its dependencies via
module.exports
Why publish unbundled files?
Most applications consuming Angular 2 libraries will probably have a frontend build based on some combination of gulp, Jspm, SystemJs or Webpack.
For those cases its best if the consumer of the library itself does the bundling. This ensures that bundlers like webpack will give optimal results for the end user, and that they won’t throw warnings.
Should bundled and minified files also be published?
Its a good idea to also publish the library as a single concatenated file, together with a minified version of it. This will allow library consumers that don’t use a frontend build to still be able to use the library.
Setting up the Typescript compiler
Since version 1.5, Typescript now allows to compile the ES6 module syntax into any module format. For example, this is the standard way of importing a component using ES6 syntax:
import {HelloWorld} from 'angular-library-example/components';
This can then be transformed by the compiler into different module syntaxes depending on the target module format. For example this is the output for CommonJs:
var HelloWorld = require('angular2-library-example/components').HelloWorld
We just need to configure the Typescript compiler to output CommonJs modules. Let’s go over the whole Typescript configuration of the sample library:
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es5", "module": "commonjs", "moduleResolution": "node", "removeComments": true, "sourceMap": true, "outDir": "../lib", "declaration": true } }
This config ensures the following:
emitDecoratorMetadata
andexperimentalDecorators
enable the use of annotations such as@Component
target: 'es5'
means that the output of the compilation is just plain Javascript, instead of for example ES6module: 'commonjs'
means that each file will be exported as a separate CommonJs module. for exampleHelloWorld.ts
will be converted into an isolated modulemoduleResolution: 'node'
ensure that the import paths will work exactly the same way in the browser as in a node runtimeoutDir
is where the output of the Typescript compiler is savedsourcemap: true
triggers the generation of source mapsdeclaration: true
triggers the generation of the typings*.d.ts
files, that enable IDE Intellisense
Creating the public API of a library
The sample library only contains the HelloWorld
component, importable from the components
barrel. Each library should provide at least one import barrel in order to:
- simplify the import of library components via IDE Intellisense
- Make it clear which components of the library are meant to be used publicly, and which are just internal implementation details
Publish components via CommonJs
As we saw all components of the library will be inside the lib folder. To make them publicly visible in the angular2-library-example/components
barrel, we need to create a components.js
in the root of the module:
exports.HelloWorld = require('./lib/HelloWorld').HelloWorld;
Inside the components.js
barrel file, we export using the CommonJs
exports
syntax any component that we want to make public. In this case only the HelloWorld
component is published.
Informing Typescript of library content
The file components.js
would be sufficient to use the HelloWorld
component, but to enable IDE Intellisense we also need to publish components.d.ts
:
export * from './lib/HelloWorld';
This will allow auto-completion when importing HelloWorld
. Next lets see how we can publish the CSS of a component.
How to publish CSS of an Angular 2 component
Angular 2 components work by default in emulated view encapsulation mode. This means that by default, the styles of Angular 2 components are isolated and don’t affect the rest of the page. Instead those styles will (in almost all cases) affect only the component itself.
How does emulated CSS encapsulation work?
Take for example the HelloWorld
component:
@Component({ selector: 'hello-world', styles: [` h1 { color: blue; } `], template: ` <div> <h1 (click)="onClick()">{{message}}</h1> </div>` }) export class HelloWorld { ... }
If we run one of the examples of the sample library, we can see that the color:blue
inline style is being applied in the following way:
h1[_ngcontent-rgt-2] { color: blue; }
The value _ngcontent-rgt-2
is an automatically generated attribute that Angular assigned to the outer most element of the template:
<h1 _ngcontent-rgt-2="">Hello World!</h1>
So this is how emulated encapsulation works! Its a simple trick applied at runtime to ensure that the component styles all have at least one attribute selector. This increases a lot the specificity of the component styles, making it unlikely that they ever get overridden.
How to override component CSS if needed
Although the style of a component is well isolated, it can still be easily overridden if necessary. For that, we just need to add an attribute to the body of the page:
<body override> <app></app> </body>
The name of the attribute can be anything. No value is needed and the name override
makes it apparent what its being used for. To override component styles, we can then do the following:
[override] hello-world h1 { color:red; }
As we can see that although the component styles have a high specificity, they can still be easily overridden if its needed – and there is always some corner case for that.
How to publish component CSS then?
The current simplest way to write component styles is inline as shown above. Here are several reasons for this:
- writing the styles and templates inline encourages the components not to grow too large
- if the CSS (or the template) starts to grow big, its a sign that the component should be split in several components
- its very convenient to have everything related to the component in a single file (like in React)
- the Angular 2 template transforms mechanism is not yet ready (its scheduled for final). But when this is ready, templates and CSS will likely be inlined everywhere via plugins, like the case of templates and the various template cache plugins in Angular 1
The convenience of distribution, combined with the good isolation and the ease of override make the combination of inline styles and emulated encapsulation a good solution for publishing component CSS.
Publishing a library to npm
Once the library is built in the lib
folder, it’s time to distribute it. You probably have configured .gitignore
to exclude the lib folder. But npm uses .gitignore
by default, so the lib folder won’t be published!
We need to start by creating an .npmignore
file that says what should be ignored when publishing to npm:
examples node_modules src
In this case, all folders where ignored except the lib
folder.
Creating a build script
In the case of a component library, we can probably keep the build very simple. Using npm scripts is likely sufficient:
scripts: { build: "rm -rf lib && tsc -p src" }
This script can be run using npm run script
, and it creates a lib
folder ready to publish. Tools like webpack can also be used effectively via npm scripts.
If the approach becomes cumbersome, then its better to introduce gulp. But for most components, probably npm scripts will be sufficient.
npm semantic versioning
When publishing new versions of the library to npm, its important to use the built-in npm version command line tool according to semantic versioning:
npm version [<newversion> | major | minor | patch]
This tool will update the package.json
version, and commit the change to git.The convention for versioning is the following:
- major: breaking changes
- minor: new features, backwards compatible to current major
- patch: no new features, only bug fixes (and still backwards compatible)
Publishing to npm
In order to publish to npm, first create an account in npmjs.com.
Then setup your username in password in your local npm:
npm adduser
When this is done, you can simply do:
npm publish
This is the essential of how to publish component published to npm. Its important to follow semantic versioning to avoid breaking the builds of library users.
How to consume an Angular 2 library
Many library consumers will probably be using a module loader and a package manager.
Lets see how the sample library can be consumed using the two most commonly used tools: SystemJs and Webpack. In both cases, the first step is to install the library using npm:
npm install angular2-library-example
Consuming a library using SystemJs
See here for an example of how to load the sample library using SystemJs
SystemJs is the most used module loader in many of the Angular 2 examples currently available. See this post for an introduction to SystemJs and the associated Jspm package manager.
Ideally we would install Angular 2 with one command via jspm:
jspm install angular2
.
But this is currently not working, see for example this issue. What currently works is to load Angular 2 with some script tags:
<script src="/node_modules/angular2/bundles/angular2-polyfills.js"></script> <script src="/node_modules/es6-shim/es6-shim.min.js"></script> <script src="/node_modules/systemjs/dist/system.src.js"></script> <script src="/node_modules/angular2/bundles/http.min.js">
Then import any libraries like the sample library via System.config
:
</script> <script> System.config({ defaultJSExtensions: true, packages: { "/angular2": {"defaultExtension": false} }, map: { 'angular2-library-example': 'node_modules/angular2-library-example' } }); </script>
And then bootstrap the app:
System.import('build/App')
A good place to keep track of what is currently possible with SystemJs is to check the scaffolding of angular-cli here over time. The issue with Jspm will probably be fixed soon.
Consuming a library using Webpack
Here is an example of how to consume the sample library using Webpack
The Html needed for bootstrapping an Angular 2 app is very simple:
<script src="bundle.js"></script>
In the case of the sample library example, the bundle file does not exist in the file system, its in memory in the webpack-dev-server
tool.
This bundle is created according to the following webpack configuration:
module.exports = { entry: "./src/app.ts", output: { filename: "bundle.js" }, devtool: 'source-map', resolve: { extensions: ['', '.webpack.js', '.web.js', '.ts', '.js'] }, module: { loaders: [ { test: /\.ts$/, loader: 'ts-loader' } ] } };
This configuration does the following:
- sets up a typescript loader named
ts-loader
, that allows for webpack to compile typescript files while loading them - activates source maps
- defines the entry point of the application (
app.ts
)
One note on using webpack to import a local version of a library using npm link: You will likelly run into cannot find module
issues. Check here for a solution.
Conclusions
With the set of practices presented above, its definitely possible to publish an Angular 2 library today. Check some early examples of libraries that where the basis for this post:
- ng2-pagination by @@michlbrmly – an Angular 2 port of the Angular 1 paginate directive
- ng2-translate by @OCombe – An implementation of Angular 1 ng-translate, for internationalizing Angular 2 applications
The best practices for publishing libraries will probably evolve over time, its best to check angular-cli from time to time, to keep track of best practices and see what scaffolding is available.
Reference: | How to create an Angular 2 component library, and how to consume it using SystemJs or Webpack from our WCG partner Aleksey Novik at the The JHades Blog blog. |
In the “Consuming a library using SystemJs” section, there is a dead link to the other article that discusses jspm and systemjs.
Please check again the link is now fixed.