Wanna create Angular Elements? Here’s how!
The “from-scratch” guide to authoring Web Components with Angular (in 10 steps)
Recently there’s been a lot of talk about the possibility of creating Web Components from a regular Angular component, but so far there are much fewer practical examples as to how to build one–in the sense of actually making the bundle that you could just add to an html and have a working custom element. It should be very easy in the future but for now we need to make do with some manual tinkering.
I do recommend you to read/watch linked presentations to learn about the specific possibilities of the technology (spoiler: they are endless 😉) but in the meantime let’s build a component for ourselves, with a focus on a minimal tooling thet we should need:
Note:
This is a from-scratch approach. I will follow this article with a setup based on Angular CLI project in separate post later this week.
Update: The CLI-based article is ready!
1. Initialize the project:
mkdir angular-element-demo
cd angular-element-demo
npm init -y demo
2. Install main dependencies:
Add the minimal amount of regular Angular family members:
(we need the latest, not yet officially released version 6 for this…
update: Angular 6 has been released, I’ve updated the article to reflect that)
npm i @angular/{core,compiler,common,platform-browser,platform-browser-dynamic}
… and friends:
npm i core-js rxjs zone.js
Now, for the elements we will need the Angular library and a polyfill:
npm i @angular/elements @webcomponents/custom-elements
3. Install dev dependencies
Let’s see: we will need Angular compiler, webpack
, ngtools
to combine them.. oh, and TypeScript:
npm i -D @angular/compiler-cli @ngtools/webpack webpack webpack-cli typescript@~2.7
Ok, enough installing for now. Now let’s write configurations.
4. Configure webpack
Again, as small configuration as possible–whatever is not necessary we skip, and we use whatever defaults webpack 4 provides:
(never heard of webpack? See its documentation for the details on the options set)
The minimal webpack.config.js
that we need is:
module.exports = {
module: {
rules: [{test: /\.ts$/, loader: '@ngtools/webpack'}]
},
resolve: {
extensions: ['.ts', '.js']
},
plugins: [
new NgCompilerPlugin({
tsConfigPath: './tsconfig.json',
mainPath: './src/index.ts'
})
],
mode: 'production',
stats: 'errors-only' // there are… few deprecation warnings that
}; // I expect to go away soon (Angular 6.0?)
// for now, let's not see them ;)
5. Configure TypeScript
Our minimum viable tsconfig.json
looks like this:
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"target": "es2015",
"lib": ["es2017", "dom"]
}
}
That’s the end of our required setup! Wasn’t that bad, I hope?
Now let’s get to the code. Our bundle will be built from only 3 source files:
the component definition, module definition and the bootstrap code.
6. Component definition
You could put really almost any component here, the important thing is to use ViewEncapsulation.Native
so that the styles are bundled in one js file with the component’s template and logic. Other than that there is absolutely nothing special about it.
For our purpose we’ll use something simple that will also illustrate Angular’s Input and Output logic working within native web component.
The src/button.component.ts
looks like this:
7. Module definition — the interesting part
So this is the one part that is the most crucial, and specific to Angular Elements, of this whole setup. We will use the Angular’s createCustomElement
function to create a class that can be used with browsers’ native customElements.define
functionality.
Angular documentation describes this best:
createCustomElement
Builds a class that encapsulates the functionality of the provided component and uses the configuration information to provide more context to the class. Takes the component factory’s inputs and outputs to convert them to the proper custom element API and add hooks to input changes.The configuration’s injector is the initial injector set on the class, and used by default for each created instance.This behavior can be overridden with the static property to affect all newly created instances, or as a constructor argument for one-off creations.
The src/button.module.ts
file:
What is also special about this module is that, since our ButtonComponent
is not a part of any other component, and is also not a root of an Angular application, we need to specifically tell Angular to compile it: for this we put it on the entryComponent
list.
We also need to tell Angular to use this module for bootstrapping, hence the ngDoBootstrap
method.
8. Bootstrap code
As for the bootstrap, nothing extremely complex going on here. Just your typical bootstraping code, with only the addition of custom-element shim.
The src/index.ts
file:
9. Build
All pieces are now in place, we just need to blast off webpack. Let’s add a build script to package.json
file:
"scripts": {
"build": "webpack"
}
and run npm run build
.
…
Ok, now what? We should have a main.js
file created in the dist/
folder, but does it even do anything? Let’s try to run it.
10. Running the code & some optimizations
Since we have an output and input defined we will put our custom element on the page, listen to it’s event, and change it’s attribute’s value in our sample code:
The [projectFolder]/index.html
file will look like this:
To serve this page we will use a simple http-server
npm module, so let’s install it: npm i -D http-server
and add to npm scripts in package.json
:
"scripts": {
"build": "webpack",
"serve": "http-server --gzip"
}
Now run npm run serve
, visit http://127.0.0.1:8080
with devtools opened and click the button to see the even’s handling.
Let’s take a moment to enjoy our (hopefully!) moment of triumf 🤓
Ok, you might have noticed that the dist bundle weights around 325kB. That’s heavy! In a not so distant future we will be able to get this down to much, much, smaller size thanks to Angular Ivy rendering engine that is planned for Angular 7 release, but for now we can at least minify and gzip what we have.
For this let’s install 2 last dev dependencies:
npm i -D uglifyjs-webpack-plugin compression-webpack-plugin
and use them within our webpack.config.js
:
After running npm run build
again, we should now have a bit (well, several bytes actually) smaller js file, but what’s more important also an additional gzipped file, that will be the one that we will serve to our users — and that one is a much lighter 85kB. It’s not perfect, but it is a substantial improvement.
Btw. you can browse the completed code on Github.
Epilogue
Phew! That was quite a ride! Note that we did it by hand and from scratch.
It should, and will, be much easier in the future. We can expect everything to be configured within the Angular CLI, and creating an element build will be a matter of a single cli command.
But if you started hearing about Angular Elements and was eager to try them out — that’s one of possible solutions. As I mentioned, I will share the second one in a following article.
Did you learn something new? If so please:
→ clap 👏 button below️ so more people can see this
→ follow me on Twitter (@sulco) so you won’t miss future posts!
See the CLI-based approach here: