A Brief Overview

Web components are the next big thing everyone is talking about these days. They’re a set of standards that allow us to write modular, reusable, and encapsulated HTML elements, natively in the browser.

These days, thanks to the Angular elements package, we can create native custom elements from Angular components.

The @angular/elements package exports a createCustomElement() API which acts as a bridge from Angular’s component interface and change detection functionality to the built-in DOM API.

If you’re not familiar with Web Components, I recommend reading this article beforehand.

How to Transform Angular Components into Web Components

Let’s see an example of how we transform an AlertComponent written in Angular into a Web Component. First, let’s create the component:

This code is pretty straightforward. We use the basic bootstrap styling for alerts, and wrap it in an Angular component. Now let’s tell Angular that we want to transform into a Web Component:

We clear the bootstrap array property and use the ngDoBootstrap hook to convert our Angular component to a Web Component by using the createCustomElement function, passing the component and the current injector we obtained via DI.

Next, we register it in the browser by using the native customElements.define() API method, passing it the element tag name and the transformed Web Component.

Now we can use it like any other HTML element. For example, we can declare it statically in our index.html file:

Or create it dynamically using the native JS API we’re all familiar with:

This was a quick 101 about using Web Components with Angular. Now that we have the basics, it’s time to dive in and understand how this whole magical process works. We’ll do so by building our own createCustomElement implementation.

Recreating the Web Component Creation Process

First, let’s create a function that returns — you guessed it — a Web Component:

To begin with, we define a class called NgElement , which extends HTMLElement . In the constructor definition, we call super() so that the correct prototype chain is established.

Now when we call our function, we’ll get a Web Component that currently does nothing, and we can register it in the browser:

This is a start. Now, let’s declare three of the component’s lifecycle callbacks that we’re interested in:

Let’s explain the role of each one. The connectedCallback is called whenever the element is inserted to the DOM, and the disconnectedCallback is called whenever the element is removed from the DOM.

The attributeChangedCallback is called whenever one of the element’s observed attributes is added, removed, or changed. We can observe an element’s attributes by implementing a static observedAttributes property.

Let’s add it to our code:

For now, we’ll leave it empty. Later, we’ll see how we fill it with the names of the Angular component inputs. Now that we’re done with the boring part, we’re ready to get down to business.

Create the Angular Component

We’ve come to the part where we need to create the component and add it to the DOM. Let’s see how we do that:

This is the usual process that hopefully you all know and have used before to create dynamic components in Angular. Our function arguments are an HTML element, a component, and an injector.

First, we create a new injector and set the passed injector as its parent, so we’ll have access to its providers.

Next, we use the injector to obtain a reference to the ComponentFactoryResolver provider. The resolveComponentFactory() method takes a component and returns a ComponentFactory. You can think of ComponentFactory as an object that knows how to create a component.

The ComponentFactory exposes the create() method which takes an injector, a multi-dimensional array which contains DOM elements that should be projected (i.e. when using ng-content ), and an HTML element, to which we append the component template.

Then, we invoke the component’s detectChanges method and add its hostView to the application. This will make sure Angular checks its view upon each subsequent CD cycle.

Finally, we return the component reference.

Now, let’s use it in our customElementPlease function:

We call the initializeComponent function, passing a reference to this , which in our case is the <my-alert> HTML element, along with the component and the injector.

The reason we call this method both in connectedCallback and in attributeChangedCallback is that when the component doesn’t have any inputs, the attributeChangedCallback function won’t be called. Otherwise, it will be called before connectedCallback .

At this point, we should see our component on the screen:

We’ve rendered the component, but we can’t interact with it yet. We want to be able to use the standard Web Components API to set attributes or properties, as well as listen to the element’s events, so let’s add this functionality.

Transform the Component’s Inputs

As our web component is just an HTML element at its core, users can set its attributes either by declaring it statically in the template or by calling the native setAttribute method:

When they do so, we want to know about it and update our Angular component accordingly. As we’ve learned earlier, to get a notification whenever an attribute has been changed, we need to return an array which contains the names of the attribute we’re interested in from the observedAttributes field.

So our first task is to map the Angular component’s inputs to an array containing the input names. Let’s see how we can do that:

We use the getComponentFactory() function we created earlier to obtain a reference to the component’s factory. One of the properties we get from the factory is the component inputs . In our case, it will look like this:

You might wonder what’s the templateName property. In Angular, we can define an alias for an input which will be the name we use in the template. It can be different from the name we internally use in the component itself. For example:

In our case, we don’t use this feature, so propName and templateName are identical. Now that we have the component inputs , we can normalize them into an HTML attributes form:

When running the above function with our inputs, we’ll get the following output:

The resulting object’s keys are what we need to pass to the observedAttributes property:

Now, we need to wire up the part that updates our Angular component whenever one of the attributes is modified:

when the attributeChangedCallback is triggered, it’s just a matter of setting the new input value and calling detectChanges . Let’s see this in action:

This is working as expected — we’ve added support for setting the attributes. But we’re still missing the second part of the puzzle; We need to support cases when we update one of the element’s properties. For example:

Note: If you don’t know the difference between attributes and properties, read this article.

Luckily, in order to add support for custom properties, we just need to add setters and getters for the inputs we want to support:

It’s as simple as that. Let’s see it in action:

Now, let’s connect the events:

Transform the Component’s Outputs

Getting the component’s outputs is a similar process to that of getting its inputs :

We obtain the component’s output names and aliases from the factory, and map them to Observables emitting the alias name and corresponding event value. Using the merge operator to merge them into a single observable. Now we can subscribe to the merged stream in our Web Component:

Whenever one of the component’s outputs emits a value, we create a custom event using the alias name and value, and use the native dispatchEvent method to dispatch it to everyone who might be listening. Now we can listen to any output’s emitted values using the native addEventListener API:

How cool is that? 😎

Prevent ‏Memory Leaks

We can’t conclude without making sure we haven’t created memory leaks. When the component is detached from the DOM, we need to destroy it and unsubscribe from the outputs’ subscription:

What’s Left?

We’ve come a long way; We can render a component, and use its inputs and outputs in conjunction with the Web Component API. There are two topics that we haven’t covered:

The first one is ngOnChanges support. In a nutshell, we should aggregate the changes of each component input and run ngOnChanges(inputChanges) when we run detectChanges() .

The second is ng-content support. In our implementation, when we created the component, we passed an empty array. Here’s a quick reminder:

In the original implementation, there’s a function that extracts the ngContentSelectors from the component’s factory, looks for the child nodes that match each selector, and map it to the expected multi-dimensional array of type Node[][] .

In both cases I encourage you to have a look at the source code and understand how it works.