Angular.js

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 and components.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 and experimentalDecorators 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 ES6
  • module: 'commonjs' means that each file will be exported as a separate CommonJs module. for example HelloWorld.tswill be converted into an isolated module
  • moduleResolution: 'node' ensure that the import paths will work exactly the same way in the browser as in a node runtime
  • outDir is where the output of the Typescript compiler is saved
  • sourcemap: true triggers the generation of source maps
  • declaration: 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:

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.

Aleksey Novik

Software developer, Likes to learn new technologies, hang out on stackoverflow and blog on tips and tricks on Java/Javascript polyglot enterprise development.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
reader
reader
8 years ago

In the “Consuming a library using SystemJs” section, there is a dead link to the other article that discusses jspm and systemjs.

Eleftheria Drosopoulou
Reply to  reader

Please check again the link is now fixed.

Back to top button