Creating Reusable Web Components with Angular Elements
A comprehensive guide to creating reusable web components with Angular elements for increased portability and reusability.
Almost four years ago, I wrote an introductory article on setting up Angular Elements when they were first introduced in Angular v6. In the years since, Angular has evolved eight times, with corresponding changes in web development. This brings us back to the need for continued learning 📚.
Table of contents:
- Why we need Angular Elements?
- Popular use cases of Angular Elements - How to create Angular Elements?
- Mental Model - Beginner 🌱
- Creating Angular Element 🔮
- Conversion Process ⚙️
- Build and Run 🔧
- Summary 🎈 - Intermediate 🌿
- Summary 🎈 - Advanced 🌴
- Summary 🎈 - Final thoughts 💭
Why we need Angular Elements?
Before we understand the why of Angular Elements, we should first understand the concept of Custom Elements (or Web Components).
Custom Elements are the key feature of Web Components Standard which allows you to define your own custom HTML elements.
If you want to create your own new HTML element with tag <video-carousel>
, you can do it with the help of custom elements and browser will have no issue with this.
A custom element extends HTML by allowing you to define a tag whose content is created and controlled by a JavaScript class.
The browser maintains a CustomElementRegistry
of defined custom elements, which maps an instantiable JavaScript class to an HTML tag.
- It provides a level of encapsulation and abstraction to any functionality on HTML page.
- It also provides a high level of reusability which improves the application’s extendibility. So rather than dealing with too many DOM elements, you just need to use your custom element. In modern framework terms, they are similar to components.
Custom Elements are great when you are dealing with plain HTML, CSS and JS and need a component-agnostic way to reap the aforementioned benefits. But what about your Single Page Applications (SPAs)? They are created with modern tools and have these benefits built-in in form of components and are widely used for web development.
How would you use your SPA functionality outside in a different application (non-SPA or SPA written in different framework or library)? Or the other way around, using custom elements within your SPA?
Achieving this with custom elements will be quite cumbersome as you need to handle a lot of work (data binding, event listeners, refreshing views, state management etc.), that in general, is handled by your framework or library that you use to develop your SPA. Wouldn’t it be nice if we can just focus on development of our components and our framework or library takes care of the heavy lifting required to achieve this? Angular team responded to this with — Angular Elements 🎉
Angular Elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.
Pay close attention to framework-agnostic for a bit, as transforming Angular components to Angular Elements makes the complete Angular framework available to the browser for dealing with change detection and other functionalities. So irrespective of where you consume them (in a non-SPA or a SPA made without Angular) they take their self-contained Angular framework making them compatible across frameworks. We can say:
Angular Elements = Angular Framework + Custom Elements
Popular use cases of Angular Elements
- The widely used scenario is to export your app as a single bundle file to be consumed by your clients as a third-party dependency. For e.g., a chat widget which you have hosted on a CDN (or as NPM package) for your clients to use on their website to handle sales traffic. These days advanced applications such as complete text editors are also exported with this method.
- Our favorite— Micro-frontend ❤️. You can export each of your shell applications as Angular Elements and can use them in your other applications.
- Websites that are mostly static in nature but have few dynamic parts such as audio/video player, charts, etc. which need to be reused at several places. You can build them once and use anywhere.
How to create Angular Elements?
After understanding the practical use cases of Angular Elements and why you need them, it’s now time to see it in action.
Mental Model
For this article, we would complete our implementation in 3 phases:
- Beginner: This phase will contain the complete set up related to Angular Elements and creating a basic component and transforming it into Angular Elements.
- Intermediate: This phase will integrate Angular Material dependency to our component to demonstrate how Angular Elements seamlessly work with other Angular firsthand packages.
- Advanced: This phase will further add a third-party dependency into our component and then transform it into elements.
The latter two phases are most likely to be used in your day-to-day work. At each step, we will use our generated Angular Element in a demo app to verify the changes.
Beginner 🌱
- Let’s begin by first installing the latest version of Angular CLI (v14 or later would do) globally.
If you already have an older version installed, please uninstall it first by runningnpm uninstall -g @angular/cli
.
To install Angular CLI run command:npm install -g @angular/cli
. - To check the installed version, open new terminal tab and run:
ng version
- Create a new project by running
ng new angular-element
.
This will ask you few questions:Would you like to add Angular routing?
NoWhich stylesheet format would you like to use?
SCSS. - Navigate to the project directory
cd angular-element
.
Now add Angular Elements by running command:npm install @angular/elements --save
- Let’s create our component with module which will be transformed to custom elements.
Run:ng g m login --module app.module
ng g c login --view-encapsulation ShadowDom --module login.module --skip-tests
.
Here we are passing--view-encapsulation
asShadowDom
because we don’t want leaking of styles from the parent components to our component or vice-versa. As we discussed earlier, the most common use case of Angular Elements is consumption by a third-party application. We don’t want such applications to impact our functionality.
Another option provided is--skip-tests
which avoids the generation of component spec file as it is out of the scope of this article. - Export the generated
LoginComponent
fromLoginModule
by mentioning it in theexports
array. - Remove the default placeholder content from
app.component.html
with<app-login></app-login>
Till now we have set up a blank login component to render as default whenever the app is served. It displays login works!
. If you get all things right, let’s add a form to our login component.
It will be a basic form with two fields — Email and Password, along with a submit button. In component’s TypeScript file, we will add:
- An input property,
submitColor
, which accepts the color value for our form submit button - An output property,
formSubmitted
, which emits the form value on submit
The content of the Login component will be as follows:
Now change the content of the app.component.html
and app.component.ts
to handle the Login component:
We have passed the color value as gold
in [submitColor]
and are listening to (formSubmitted)
event to print the value of the form. Hit save all and run ng serve
in terminal. You should see the below screen on serve:
Creating Angular Element 🔮
It’s time that we all have been waiting for. Let’s convert our Login component to an Angular Element. To achieve this, we will first add package ngx-build-plus
, it will help us to create a single bundle file.
Run: ng add ngx-build-plus
This command will replace the builder value from @angular-devkit/build-angular
to ngx-build-plus
for all the configurations of your application. Since the latter extends the default Angular CLI build behavior, all the options of default builder are supported along with its own add-ons.
Any feature during its lifecycle spends most of the time in development phase and only a proportion of time in deployment phase. This holds true for our case as well. To make the development experience seamless, we will use two workflows — Serve and Build.
Following is the app build flow diagram:
- Serve Flow
This flow is run when app needs to be served locally and changes needs to be hot-reloaded in the browser, like any normal Angular project development. In this flow,AppModule
is the bootstrap module &AppComponent
would be the bootstrap component that would be rendered on the browser. - Build Flow
This flow is specific to Angular Element creation, where the bootstrap module isElementModule
instead ofAppModule
. The reason for this is the responsibility of registering the component as a custom element in the browser.ElementModule
registers the Login component as custom element in the browser upon instantiation, which is then rendered.
💡 If we register the component in the AppModule, it would be registered in the browser’sCustomElementRegistry
even during the development phase. To maintain separation of concerns, we want only our generated bundle file to register the component in the registry.
To begin with, the following changes need to be made:
Modifications in file angular.json
:
- Copy the project
angular-element
withinprojects
and paste it as a new entry with nameangular-element-serve
. This new entry will be used for serving our application or Serve flow. - Replace all the occurrences of
angular-element
withangular-element-serve
within the new projectangular-element-serve
. - Change the
build
configuration'sbuilder
value within projectangular-element
tongx-build-plus:build
. Also add"singleBundle": true
inoptions
of the builder. - Modify the
main
entry value tosrc/main.element.ts
for the projectangular-element
. - Remove the
polyfills
entry from theoptions
ofangular-element
. - Set
outputHashing
tonone
inproduction
configuration ofangular-element
. - Remove the
assets
,styles
andscripts
arrays from theoptions
array for the projectangular-element
as we don’t need them.
💡 In above steps, we’ve fine-tuned our angular.json
file to make it capable of creating Angular Elements:
- We are telling Angular that there are two projects —
angular-element-serve
&angular-element
. This is required in order to achieve our Serve and Build flows as we need to use different configurations for these projects.
To serve the application run:ng serve --project=angular-element-serve
To build the application run:ng run angular-element:build
- The option
singleBundle
inngx-build-plus
makes a single bundle file of the complete build. - The reason of removing
polyfills
is to limit the build artifacts to a single file, as otherwise, it would includepolyfills.js
in the final build. We will add the required Zone.js in our main file in next steps to compensate for the removed polyfill. - Setting output hashing to
none
would keep the final bundle file name to justmain.js
and would not include hash in its name.
New files to add:
- Create a new file
src/app/element.module.ts
at the same level asapp.module.ts
and export aNgModule
classElementModule
from it. We will get back to its implementation in a bit. - Create a new file
src/main.element.ts
at the same level asmain.ts
in the project hierarchy. To maintain our flows, we need to specify respective entry points for them and thus differentmain
files.
Copy the contents ofsrc/main.ts
in this new file and replaceAppModule
withElementModule
.
Also add import of Zone.js —import 'zone.js';
- Create a new file
tsconfig.element.ts
at the root level as/tsconfig.app.ts
in the project hierarchy.
Copy the contents of/tsconfig.app.ts
in this new file and replacesrc/main.ts
withsrc/main.element.ts
withinfiles
array.
Also remove the entrysrc/polyfills.ts
from it.
💡 In above steps, we’ve created new files related to Build flow that will help us in creating custom elements: element.module.ts
, main.element.ts
, tsconfig.element.ts
. To differentiate them from normal files, we have included element
keyword in their names.
If you got everything right, following will be the project hierarchy:
And the code of respective files (to keep the snippet of angular.json
short, I have only included the build
and serve
configuration of both the apps):
Till now we’ve covered the things we require from our builder to support Angular elements creation. Let’s now see the conversion process from a component to custom element.
Conversion Process ⚙️
Angular provides function createCustomElement()
for converting Angular component to custom element. The function collects the component with its dependencies along with the Angular functionality the browser needs to handle component instances. Following is the conversion process that takes place:
- It takes the
Input
property of the component and converts them to corresponding attributes of the custom element. The attribute names are in dash-separated lowercase. So,@Input() submitColor
would become attributesubmit-color
on custom element. A change hook is also added to handle input changes. - It takes the
Output
property of the component and converts them to HTML custom events with the name same as the property. So,@Output() formSubmitted
would dispatch eventformSubmitted
on the custom element and the emitted data can be accessed via event’sdetail
property. - It returns a constructor class that implements interface
NgElementConstructor
. It holds all the important information related to the component for its working and can be registered with the browser’sCustomElementRegistry
. - Once the constructor class is obtained, we need to pass this to the built-in
customElements.define()
function along with the custom element tag for its registration inCustomElementRegistry
. This would register the configured constructor class against the provided tag. Once the browser encounters the custom tag it uses the constructor class to create the custom element instance and render it in the DOM.
⚠️ Avoid using same name in your
@Component
selector and for your custom tag while callingcustomElements.define()
, as doing so will generate two component instances for single DOM element. First will be your regular Angular component (added by the framework) and the second will be your custom element (added by the browser).
Let’s now head towards ElementModule
, as this module is responsible for registering component to custom element registry:
Step 1: ElementModule
needs to implement DoBootstrap
interface. DoBootstrap
is a hook that is used for manual bootstrapping of components. This hook is invoked when no component is provided in the bootstrap
array.
Step 2: Now implement the method ngDoBootstrap()
in ElementModule
. This method will be used for bootstrapping. Here we would write our conversion logic that we have discussed above.
Build and Run 🔧
It’s time that we see all this hard work in action.
Run command:ng run angular-element:build:production
This will create dist/angular-element
folder at the root of the project with main.js
and index.html
file.
💡 You can use this
main.js
file anywhere you want such as any other application or Bit, and it would render your login component! With Bit you can “harvest” both regular Angular components as well as web components from your codebase and share them on bit.dev. This would let you reuse your code across multiple projects, considerably reducing boilerplate.
Learn more here:
A simple example of it would be:
The above code does the following:
- It adds our bundle file
main.js
created earlier which will take care of registering our custom element in the browser’s registry. - Adds the custom tag
login-provider
withcyan
button color. Browser encounters this and fetches the details from the registry and renders it in the DOM. - Adds a script to interact with the custom element. It listens for the
formSubmitted
event emitted by the custom element. It prints the data received and turns the button color topink
on submission.
Summary 🎈
See how beginner-level that was? (pun intended)
On a serious note, I understand that we have covered a lot in this phase, but it is unavoidable as we need to clarify the fundamentals. Before we move on, I kindly request you to go through it once again to fully understand the entities involved and how they interact with each other. Once you have a clear foundation, the next phases will be easier, trust me! 😉
Intermediate 🌿
In this phase, we will integrate Angular Material in our project to see how Angular Elements work with other Angular packages.
Run command: ng add @angular/material --project=angular-element
.
We get the following error in our terminal:
Your project is not using the default builders for “build”. The Angular Material schematics cannot add a theme to the workspace configuration if the builder has been changed.
💡 Angular CLI complains because we are using ngx-build-plus
builder instead of its default @angular-devkit/build-angular
builder.
To overcome this, either of the following ways can be taken:
- You can add the Angular Material manually (not recommended).
- Or temporarily change the instance of
ngx-build-plus
builder with@angular-devkit/build-angular
in yourangular.json
. Make sure to replacengx-build-plus:build
with@angular-devkit/build-angular:browser
. - Re-run the command
ng add @angular/material --project=angular-element
. This time it will finish successfully. - Revert the changes back to
ngx-build-plus
at all the places.
The above add
command will make changes to files index.html
, package.json
, angular.json
and styles.scss
. Make sure to add BrowserAnimationsModule
in your AppModule
& ElementModule
.
Let’s make changes to our login component to make use of material components:
Serving the application gives following output:
Angular Material doesn’t work! 😢
On inspection 🔎, we can find that the material styles from the imported MatButtonModule
, MatInputModule
, etc. are loaded properly within the shadow root of our login component but still the styles are not working:
The culprit here is the material theme. Since the theme is globally defined, it can’t penetrate the Shadow DOM of the login component. Thus, causing the issue. To fix this, you need to add your global /src/styles.scss
in the styleUrls
array of the login component.
And now when you hit save all…
Summary 🎈
This section was solely intended to showcase the first-class support of Angular packages with Angular Elements. You can use any Angular package, and it will work seamlessly.
Advanced 🌴
In our last phase, we will integrate a third-party package ngx-bootstrap
. We are going to use its Typeahead
module to show a typeahead country field in our login form. ngx-bootstrap
depends on Bootstrap CSS to render its components.
Let’s get it ready with the following steps:
- Run the command
npm i ngx-bootstrap@9.0.0
- Import the
TypeaheadModule
inLoginModule
- Add a new form field Country in our login template
- Import the Bootstrap CSS file at the top of our
login.component.scss
- Add a list of countries to
login.component.ts
to be used by the typeahead
Let’s see the final code:
And that’s how it looks in action:
Summary 🎈
In the last part of the article, we saw how easily we can integrate any third-party packages with Angular Elements. You just need to ensure proper compatibility between the Angular versions used in your application and the external packages, and the rest will be taken care of by Angular ❤️
Final thoughts 💭
I have tried my best to keep this article as practical as possible and to make it relevant to real-world scenarios that developers often encounter while working on features. However, your use case may be different and may involve different runtime problems. In such cases, remember that you have the power of Angular Elements at your disposal!
You can find the code here, this repo contains the branches with names same as the phases of this article — beginner
, intermediate
and advanced
.
If you have come this far, I want to express my appreciation for your discipline and dedication to learning something new. Cheers! 🍷
Did you learn something new? If so please clap 👏 button and share so more people can see this.
Build Angular Apps with reusable components, just like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: