A journey with @angular/elements

based on a true story

Bilel Msekni
6 min readJul 23, 2018

Lately, my team had a chance to use @angular/element in a real world usecase. It was a journey full of challenges, so I’m sharing our experience.

If you are unfamiliar with @angular/elements, it’s a new library introduced to help developers transform their Angular components into web components. To learn more, I suggest you take a look at the official documentation or this ng-conf talk.

Our story begins 👊

When we needed to update some business rules on our restaurant reservation form. Although it might sound simple, our form was used inside two applications: A .NET based CMS and an Angular application. Therefore, we would have to update code in two different places, using two different technologies thus costing us two times more than it should.

What if we can transform this form into a web component, that we can reuse inside our CMS and Angular apps at the same time?

That’s how we set on our journey to transform our reservation form into an Angular element.

Let’s start by describing the form, it has two fields:

  • Restaurant: To pick a restaurant type.
  • Table: To set the number of guests.

It has a provider code used everytime a client is redirected to a partner page. Finally, there is a feature flag to enable/disable big reservations (6+ guests).

Our reservation form

The form was part of the Angular application so our first step was to extract it before transforming it to an Angular element. This means identifying inputs and outputs of the component:

  • Inputs: Our decision was to provide restaurants and tables as inputs instead of having the form fetch them from the backend. Why ? Because we wanted our form to focus on its business rules while the host app takes care of retrieving data (Http, authentication…). As for the provider code and big reservation parameters, they are app specific, thus they will be provided as well by the host app.
  • Outputs: we chose to only emit an event everytime the user clicks on continue to one of our restaurants while handling partner redirection inside the element. Why? Because partner redirection doesn’t require any authentication unlike sending a reservation request to our backend.
Inputs & Outputs of our form
A snippet of reservation form component

After extracting the form 🏃

We wanted to move it to its own project. Our first obstacle was to choose between generating an application project type or a library one. Here is a summary of our comparaison:

+----------------------------+--------------------+---------------+
| Feature | Application | Library |
+----------------------------+--------------------+---------------+
| Outputs a single js bundle | false | true |
| Standalone testing | true | false |
| NPM package/publish ready | false | true |
| Supports 3rd party js | true | false |
+----------------------------+--------------------+---------------+

Maybe the library seemed most convenient at first because our element is not an application but more of a shared component. In addition, library projects are preconfigured to be published as npm packages out of the box.
Well, we were wrong 😅, because creating an Angular element relies on a polyfill (document-register-element) added to angular.json scripts array. This is something libraries don’t allow by default. Eventually, we decided to go with the application type and we were glad we did so because standalone testing was very helpful as it allowed us to see how our element behaves in a pure browser environment away from any JS framework magic.

P.S: Notice that the library type is still possible but would need customization to include the script, something we chose not to do.

To generate a new application project called elements and add @angular/elements feature

Next, we followed these steps to transform our form into an Angular Element:

  1. Delete app.component.* and remove bootstrap from AppModule
  2. Add ReservationFormComponent to entryComponents and declarations
  3. Implement ngDoBootstrap and register the form as an Angular element
AppModule (Elements Project)

4. Test it by replacing app-root with elm-reservation-form in the index.html

index.html (Elements project)

Excited to play with our new element, we started by passing some data:

And it crashed … 💥

This is where we faced our second obstacle. What happened is that when we assigned arrays, a boolean and a string to attributes, they were parsed into strings before being passed down to the element. Unfortunatly, that’s how the web component standard works, attributes can’t accept anything besides strings. Everything else will simply be parsed into strings.

Luckily, many work arounds are possible, we didn’t find a standard way so here is a list of what can be done:

  • Convert values with JSON.stringify then serialize them in the component.
  • Write a script that assigns the values via JavaScript.
  • Create global variables to retrieve them later in the component.
Creating a script to assign attributes via javascript

By trying different ways, we discovered an interesting fact. Lifecycle hooks are not triggered in the same order as an ordinary Angular component:

  • Web component cycle: Constructor => OnInit => OnChanges
  • Angular component cycle: Constructor => OnChange => OnInit

Why ? I am not sure yet but execution outside of an Angular context seems to mess up some facts. The same goes for OnPush change detection strategy, it was behaving a bit odd, so we deactivated it until further investigation.

After overcoming the string only inputs issues, we faced a mysterious bug. So unusual that we struggled for hours to find it 😩

Can you see the bug in the code above?

The bug was related to how we wrote our attribute names. Camelcase was simply not accepted, only lower case was tolerated so :

providerCode in camel

should be changed to:

providercode in lower case

Our form is finally working 👌

The next step is to use it within our Angular application. And then we asked ourselves: Aren’t we making this too complicated ? The element is made of an Angular Component and this is an Angular application, why can’t we just skip the overhead of element integration and share logic using NG modules.

So back to our code base where we created a new module ReservationFormModule and transfered necessary imports and declarations to it from the App module.

Module transformation

Notice how we only moved ReservationFormComponent declaration to the ReservationFormModule leaving entryComponents array in the app module. This is because entryComponents are not shared when an NG module is imported. Furthermore, ReservationFormModule will be imported in our Angular app so it is essential not to use entryComponents because it’s not how the component is meant to be used.

This technique was only possible because the element project and the Angular application were in the same repository. If they were in two different repositories, I guess it would make sense to use the element directly inside the Angular application.

So far, everything is working fine for the Angular application, we can start integrating the element in our .Net CMS. Using our element inside it will require importing the script but we have 5 ! Styles, runtime, polyfills, scripts and main ! Not so sexy 💩.

This is an incovenience from the application type but we quickly solved it by writing a script to merge all of these outputs into a single bundle.

Next, import the bundle in the html using a script tag and pick a way to pass data from the CMS to our web component (JSON serialization as an example)

Exchanging data through JSON

Finally, 🙌

Because web component standard is not supported by all browsers, we were afraid that our journey might not end well. However, after testing, we happily report that our form worked perfectly well on all desktops and android devices, even on IE. The only issue was with IOS 9 and below where it turned out the bug was due to a bad typescript transpilation rather than an issue with @angular/elements.

By this point, our journey has reached its end. To sum up,

We liked ❤️

  • Encapsulating logic in web components to use it across different platforms.
  • Sharing code between NG applications and elements (unlike react/preact)

We disliked 💔

  • Attributes accept string only values
  • Bundle size is still high (296 kb without gzip)
  • Poor documentation

We are excited about 💖

  • Angular CLI will provide tooling for @angular/elements in the future
  • Opens the door for micro-front end architecture

This is a link to the source code and a presentation of our journey.

--

--