Your options for building Angular Elements
with the CLI
This blog post is part of an article series.
- Angular Elements, Part I: A Dynamic Dashboard In Four Steps With Web Components
- Angular Elements, Part II: Lazy And External Web Components
- Angular Elements, Part III: Angular Elements without Zone.js
- Angular Elements, Part IV: Content Projection with Slots in Angular Elements (>=7)
- Angular Elements, Part V: Your Options For Building Angular Elements With The CLI
In this article you will learn:
- 📦 How to provide a single bundle for your Angular Elements
- 🔼 How to use polyfills for legacy browsers
- 🌿 How Ivy can help with bundle sizes (once it’s released) and where it cannot
- 🔀 How differential serving can help (not only) with Angular Elements
- 🍰 How to share dependencies between separately compiled Angular Elements
Currently, Angular Elements officially supports exposing Angular Components as Web Components — or more precisely: as Custom Elements — within Angular projects. Upcoming versions will very likely also support exporting Web Components which can be used with other frameworks or VanillaJS. I’m using the term external web component for referring to this.
In this article, I provide several strategies you can use to provide external web components already today. Some of them will definitely benefit from the introduction of Ivy in some months and some of them address different aspects.
One more time, I want to thank Rob Wormald — the father of and master mind behind Angular Elements — for discussions that led to some of those inofficial solutions.
The example used here is a variation of the dashboard tile component from my introduction to Angular Elements:
It can be found here and consists of a CLI workspace with two projects. One of them called
dashboard-tile exposes a simple dashboard tile as an external component:
The code behind it is quite simple:
In order to provide this component as a custom element when the Angular application starts up, the respective code is placed in the
ngDoBootstrap is needed because the application does not have a bootstrap component. This is because I don’t want to bootstrap an ordinary Angular component but just register a custom element with the browser.
In theory, you should be able to call the web component directly within the
index.html after exposing it that way.
In practice, you get the following error when trying this out with the starter branch of the provided source code:
Failed to construct ‘HTMLElement’: Please use the ‘new’ operator, this DOM object constructor cannot be called as a function.
This is because Custom Elements are to be used with EcmaScript 2015 and above by definition. However, in order to support legacy browsers like Internet Explorer, TypeScript has to downlevel it to EcmaScript 5. Although this might change in the future, currently EcmaScript 5 is the default setting when creating an Angular project with the CLI. To tweak this, you can set the property
target in your
tsconfig.json to ES5.
Of course, this solution is not doable if you are in the unfortunate situation where you have to support Internet Explorer. A lot of my customers face this as a company policy and hence, they have to find a way to at least officially run it in this browser of Microsoft’s former days.
In this case, we need to go with two polyfills: One polyfill enables web components in browsers that DO NOT support custom elements; the other one enables using them together with EcmaScript 5 in browsers that DO support them.
In order to also support old browsers, I’ve decided to go with the polyfills in the
@webcomponents/webcomponentsjs package. To load them with respect to the browser’s capabilities, I’ve copied them over to the
assets folder and referenced them at the end of the
polyfills.ts file in a classic way:
To automate this cumbersome task, I’ve written a schematic which is part of my community project
ngx-build-plus. To install it, use
After that you can install the polyfills with an included schematic:
The called schematic creates an npm script which copies over the polyfills and executes it. It also updates the
index.html with one of the above shown
script tags. The other one goes to the end of your
polyfills.ts file because it needs some other polyfills that are included there too.
After starting the solution (
npm start) you should see something like this in Chrome:
To make this work with Internet Explorer, we also have to uncomment the imports for the respective polyfills in the
If you also need support for animations, you have to remove some additional comments and install additional npm packages. Just follow the information provided by the comments in the
Please note, that you need to reference the
CUSTOM_ELEMENTS_SCHEMA in your respective modules if you want to use a custom element within an Angular Component:
Angular Elements comes with an alternative polyfill that is registered within your
angular.json when installing it with
ng add @angular/elements. This one is far more lightweight than the one I’m using here. However, it just can be used with browsers supporting EcmaScript 2015 and above. Hence, when you don’t need to target Internet Explorer, this one should be prefered.
Now, let’s create a bundle for our web component using
This gives us 4 (!) bundles:
While this is ok for an ordinary SPA, it’s far too much for a simple web component. In our case, having just one self-contained bundle would be better.
My above mentioned community project
ngx-build-plus provides a simple solution for this with its
After running this, we get one and only one bundle as wished:
An alternative to
--single-bundle you see sometimes is manually copying the four bundles into one file. Unfortunately, this does not work if you have more than one such meta-bundle. The reason is that webpack is exposing a global variable and this would get overwritten when using several such bundles that have been compiled separately.
When you look at the bundle sizes, you immediately realize that they are far to huge for such a simple web component. That’s because they include Angular, RxJS and other libs — at least the parts of it that have not been tree-shaken off. It’s even worse: If you compile several bundles separately, each of them get a copy of those libraries:
This is where Ivy comes in.
Beginning with Angular 8 we will get the new Ivy compiler. In this version, it will be hidden behind a flag. It makes Angular more tree-shakable and compiles the UI part of components down to code which is quite close to the DOM. For this reason, typical web components will benefit a lot from Ivy and the resulting bundle won’t need much of Angular.
In the best case, two separately generated bundles with Angular Elements will look like this:
They just contain their component code and a very tiny remainder of Angular which acts as the runtime. As mentioned: in the best case!
Ivy will enable new features in Angular, which will come gradually, and it may reduce your app size but do not expect wonders – it will not make your JS disappear.
Especially, if our components contain lots of libraries besides UI code, Ivy will not help much. Or to put it in another way: It cannot make the used parts of things like
In this case, we very likely need to find a way to share such dependencies among separately built bundles. This leads to an idea presented in one of the following sections. But first, let’s talk about a quick win which is called differential serving.
Something which is still annoying is the fact, we need a polyfill even for browsers that DO support custom elements if we want to support ES5-browsers like Internet Explorer. This issue can be solved with differential serving. That means, we are creating two sets of bundles: One set is EcmaScript 2015+ based and indented for modern browsers and the other one is EcmaScript 5 based and loaded into Internet Explorer.
By doing this, we can also ship more optimized bundles to the modern browsers: They don’t need to contain all the polyfilly and they don’t need to be downleveled to ES5 which makes them smaller.
If you have implemented the externals idea described in the two previous sections, switch now back to your former branch. Externals and Differential Serving cannot be used together for now.
In some future version, the Angular CLI will very likely support differential serving and beginning with Angular CLI 7.3 it already supports conditional polyfill loading. Until it fully supports differential serving, we can use my community project
ngx-build-modern which is nothing else than a plugin for
After adding it with
ng add, we get a
polyfills.modern.ts file alongside the already known
polyfills.ts. As the name implies, the former one will be used for modern browsers and the existing one will be used for legacy products like Internet Explorer.
Now, we can finally remove the polyfill for browsers that DO support web components as they get now EcmaScript 2015+ served.
Unfortunately, when debugging we currently get only ES 5, so the following hack is necessary. I’ve placed it at the end of my
polyfills.ts before loading the other web component polyfills:
Also, for modern (ES2015+) browsers which do not support Web Components yet (like Edge), we need to load the polyfills in the
To build everything, switch to your project’s root and run the following npm script created by the
ngx-build-modern schematic before:
If you switch to the folder
dist/dashboard-tile you should see two sets of bundles:
As you see here, the modern bundles are about 88 KB smaller. They contain less polyfills and are more compact due to not having the need of downleveling to ES5. In general you will see: the bigger the project, the bigger the difference between modern and legacy bundles.
index.html contains the necessary markup to load the right set of bundles. For loading modern bundles it uses this pattern:
Because of using
type="module" this is only respected by modern browsers.
The bundles for legacy browsers are loaded that way:
nomodule attribute makes modern browsers to ignore it.
To test your solution, run a local web server within the folder with the built application, e. g.
live-server which can be installed via npm (
npm i -g live-server).
This and the next section describe a quite advanced and inofficial process to share libraries between separately compiled Angular Elements. Make sure you really need this solution before implementing it.
In order to share libraries like
@angular/common/http which is used in our above shown Angular Element, we could load them into the browser’s global scope and reuse them in our web component bundles:
This is something that was quite usual some years ago. Think about using jQuery. We needed to load jQuery and jQuery UI once and the bundles with our jQuery widgets just referenced them.
However, Angular projects are normally built into several bundles that only know each other and other bundles cannot easily access their code.
To solve this issue, Angular’s Rob Wormald came up with a interesting idea: Let’s tweak the build process so that the generated bundles expect the shared libraries not to be part of them but are located within the global scope. In order to make this possible, we need to find a way to put Angular and its dependencies there.
Fortunately, the Angular packages format prescribes to expose Angular libraries also as UMD bundles and they do this job. In the case of Angular itself, they register themselves at
This involves a lot of manual steps I’ve automated with another schematic. Because this changes a lot of things, I’ve created an own branch for this in my demo project. It’s called
If you want to try it out by yourself, I would also branch the starter branch for this:
Than, call this command:
To compile everything, use this npm script generated by
After this, you can switch to your
dist folder and try out your solution:
If you want to prepare an Angular application that hosts such a custom element, you can use the
--host switch. E. g. the demo project contains a
playground-app which could be prepared using
You can find such an application in the
This command is more or less doing the same as the command without
--host. However, it does not switch to a single bundle or turn off hashes in file names.
There is also an npm script
copy:ce which copies over the
dashboard-tile bundle to the
playground-app where it can be dynamically loaded.
To dynamically load the dashboard tile, it creates a script tag at runtime.
Behind the covers
Now, let’s talk about what happened here. The schematic we’ve executed created a partial webpack configuration, which defines where the shared libraries can be found within the browser’s
If you npm install a newer version of Angular, just run the following script it also inserted into your
In addition, the schematic also copied over a lot of UMD bundles into the
assets folder and it referenced them within the
As an alternative, you can also consider to put those UMD-bundles into one or several meta-bundles.
There are several strategies for building web components and they differ from those normally used for building full blown SPAs. Ivy will help a lot with reducing the bundle sizes if your project mainly contains UI code. Besides this, it also improves tree-shakability in general. For sharing libraries you can use externals. The community project
ngx-build-plus helps with this and with creating a single bundle. It also helps with installing polyfills for legacy browsers. In addition, differential serving makes sure that only browsers which needs the polyfills get them. It also makes sure that modern browsers get smaller and more optimized EcmaScript 2015+ bundles.