Creating Reusable Web Components with Angular Elements

A comprehensive guide to creating reusable web components with Angular elements for increased portability and reusability.

Prateek Mishra
Bits and Pieces

--

Photo by Alex Turcu on Unsplash

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?

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.

Custom element registration process in browser
  • 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

  1. 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.
  2. Our favorite— Micro-frontend ❤️. You can export each of your shell applications as Angular Elements and can use them in your other applications.
  3. 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:

Mental model of implementation
  1. Beginner: This phase will contain the complete set up related to Angular Elements and creating a basic component and transforming it into Angular Elements.
  2. Intermediate: This phase will integrate Angular Material dependency to our component to demonstrate how Angular Elements seamlessly work with other Angular firsthand packages.
  3. 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 🌱

Photo by Joanna Kosinska on Unsplash
  • 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 running npm 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? No
    Which 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 as ShadowDom 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 from LoginModule by mentioning it in the exports 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:

Login component

Now change the content of the app.component.html and app.component.ts to handle the Login component:

App 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:

Login component UI 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:

Flow diagram of app build process
  1. 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.
  2. Build Flow
    This flow is specific to Angular Element creation, where the bootstrap module is ElementModule instead of AppModule. 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’s CustomElementRegistry 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 within projects and paste it as a new entry with name angular-element-serve. This new entry will be used for serving our application or Serve flow.
  • Replace all the occurrences of angular-element with angular-element-serve within the new project angular-element-serve.
  • Change the build configuration's builder value within project angular-element to ngx-build-plus:build. Also add "singleBundle": true in options of the builder.
  • Modify the main entry value to src/main.element.ts for the project angular-element.
  • Remove the polyfills entry from the options of angular-element.
  • Set outputHashing to none in production configuration of angular-element.
  • Remove the assets, styles and scripts arrays from the options array for the project angular-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:

  1. 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
  2. The option singleBundle in ngx-build-plus makes a single bundle file of the complete build.
  3. The reason of removing polyfills is to limit the build artifacts to a single file, as otherwise, it would include polyfills.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.
  4. Setting output hashing to none would keep the final bundle file name to just main.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 as app.module.ts and export a NgModule class ElementModule from it. We will get back to its implementation in a bit.
  • Create a new file src/main.element.ts at the same level as main.ts in the project hierarchy. To maintain our flows, we need to specify respective entry points for them and thus different main files.

    Copy the contents of src/main.ts in this new file and replace AppModule with ElementModule.

    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 replace src/main.ts with src/main.element.ts within files array.

    Also remove the entry src/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:

New/modified files in 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):

Code configuration for Angular Elements

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:

  1. 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 attribute submit-color on custom element. A change hook is also added to handle input changes.
  2. 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 event formSubmitted on the custom element and the emitted data can be accessed via event’s detail property.
  3. 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’s CustomElementRegistry.
  4. 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 in CustomElementRegistry. 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.
Conversion process from component to custom element

⚠️ Avoid using same name in your @Component selector and for your custom tag while calling customElements.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:

  1. It adds our bundle file main.js created earlier which will take care of registering our custom element in the browser’s registry.
  2. Adds the custom tag login-provider with cyan button color. Browser encounters this and fetches the details from the registry and renders it in the DOM.
  3. 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 to pink 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 🌿

Photo by Icons8 Team on Unsplash

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:

  1. You can add the Angular Material manually (not recommended).
  2. Or temporarily change the instance of ngx-build-plus builder with @angular-devkit/build-angular in your angular.json. Make sure to replace ngx-build-plus:build with @angular-devkit/build-angular:browser.
  3. Re-run the command ng add @angular/material --project=angular-element. This time it will finish successfully.
  4. 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:

Serve output after adding Angular Material

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:

Login compoent Shadow DOM

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…

Login component with material UI

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 🌴

Photo by Jelena Mirkovic on Unsplash

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:

  1. Run the command npm i ngx-bootstrap@9.0.0
  2. Import the TypeaheadModule in LoginModule
  3. Add a new form field Country in our login template
  4. Import the Bootstrap CSS file at the top of our login.component.scss
  5. 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:

ngx-bootstrap typeahead integration with Angular Element

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 💭

Readers adding the power of Angular elements to their gauntlet (resume)

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.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

Technical Lead • Angular • RxJS • NgRx • Ionic • TypeScript • JavaScript • Firebase • Electron JS • Node • Socket.io • ChatGPT • CloudAMQP • RabbitMQ