🌿📦🅰️ Web Components with Angular Ivy in 6 Steps

Source Code

This articles shows:

  • 🌿️ Wrapping an Ivy component as a Web Component/ Custom Elements
  • 📦 Producing small and optimized bundles with Ivy and the CLI

Intro and Motivation

The combination of Ivy, the new Angular Compiler, and Web Components/ Custom Elements is very tempting as it allows us to use our existing Angular knowledge to provide framework agnostic components via optimized bundles.

While Angular Elements makes exposing Angular Components as Web Components/ Custom Elements very easy, it does not support Ivy yet. Saying this, we have also to take into account that Ivy is currently in a quite early stage: Beginning with Angular 8 it's an opt-in so that we can try it out and with version 9 which arrives in fall 2019 it shall be backwards compatible and on by default.

As Angular's Rob Wormald told us in his talk at ngconf 2019, there are plans to bridge the gap between Ivy and Elements. He also showed us, how the tasks this solution will perform can be done manually today.

In this article, I'm showing in 6 steps how to implement this idea. For this, I have to leverage parts of Ivy that still have been private when writing this. As "Ivy Elements" will automate these steps, I would not consider this to be a final solution. However, many of my customers and workshop attendies want to start already now or at least build a proof of concept. For these situations, the solution outlined here will come in handy.

The example used here is a simple component displaying data about a flight:

Web Component with flight information

The source code I'm using here, can be found in my GitHub account.

Step 1: Create an Angular project with Ivy support

First, we need an Angular project that is configured to use Ivy. Angular CLI 8 which has been available as an release candidate (rc.3) when writing this comes with an --enable-ivy switch for this:

ng new ivy-project --enable-ivy

Basically, this switch adds the following section to your tsconfig.app.json for enabling Ivy:

"angularCompilerOptions": { "enableIvy": true }

To get Ivy during debugging, we need to use the --aot flag when calling ng serve:

ng serve --aot

Step 2: Provide an Angular Component

In this step we provide an ordinary Angular component that will be exposed as a Web Component/ Custom Element later:

import { ɵdetectChanges } from '@angular/core'; [...] @Component({ templateUrl: './flight.component.html' }) export class FlightComponent { @Input() flight: Flight; @Input() selected: boolean; @Output() selectedChange = new EventEmitter<boolean>(); toggle() { this.selected = !this.selected; this.selectedChange.emit(this.selected); // Calling change detection for // zone-less change detection ɵdetectChanges(this); } }

Perhaps you've seen the one aspect that is not usual here: The component is triggering change detection manually.

While the current version of Angular leverages zone.js to find out when to update data bindings, the Angular team is investigating possibilities for making zone.js optional. There are several reasons why this is beneficial: One reason is bundle size. We don't want to ship 100+ KB of zone.js (10+ KB compressed) alongside a 10 KB date time picker (you can replace data time picker with any custom element you want to provide).

Also, there are issues with zone.js and native async/await which we'll get when directly compiling to EcmaScript 2017. As most people are down leveling their code to ES5 or ES2015 nowadays, this is currently not a big show stopper. Another aspect you might not like with zone.js is that it does a lot of magic in the background.

To trigger change detection, I'm using the ɵdetectChanges function. You can tell by the starting character ɵ that it is currently still part of Ivy's private API.

Step 3: Hand-write a Custom Element wrapper for your Angular Component

Now, we can wrap our Angular component as a custom element. Currently, this needs to be done by hand while "Ivy Elements" will automate this task in the future.

To use the Custom Element API provided by almost all modern browsers, we just need to subclass HTMLElement:

export class FlightInfoElement extends HTMLElement { [...] }

Within the element's constructor, we can make Ivy to render our Angular component:

export class FlightInfoElement extends HTMLElement { private comp: FlightComponent; constructor() { super(); this.comp = ɵrenderComponent(FlightComponent, { host: this }); } [...] }

For this, this example leverages Ivy's ɵrenderComponent function which is currently private too. The passed host is the DOM element that will represent the component. In our case the Custom Element itself is the host, hence we pass just this.

Now, we have to connect the component's properties and events to respective counterparts of the custom element. In the case of our event we need to subscribe for it and raise it again as an event of the custom element:

export class FlightInfoElement extends HTMLElement { private comp: FlightComponent; constructor() { super(); this.comp = ɵrenderComponent(FlightComponent, { host: this }); this.comp.selectedChange.subscribe(eventInfo => this.dispatchEvent(new CustomEvent('selected-change', { detail: eventInfo }))); } [...] }

For the properties we have to introduce getters and setters:

get flight(): Flight { return this.comp.flight; } set flight(flight: Flight) { this.comp.flight = flight; ɵdetectChanges(this.comp); } get selected(): boolean { return this.comp.selected; } set selected(selected: boolean) { this.comp.selected = selected; ɵdetectChanges(this.comp); }

Please note that that setters also trigger change detection by calling ɵdetectChanges.

Finally, we should create attributes for all properties that hold primitive values. For the sake of demonstration, I've also done this for the complex flight property:

// Tell the browser to observe these attributes static get observedAttributes() { return ['selected', 'flight']; } [...] // Delegate to respective property when attribute changes attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'selected': // string to boolean this.selected = newValue === 'true'; break; case 'flight': // Normally, complex properties are not mapped // to attributes. This is just for demonstration // purposes. this.flight = JSON.parse(newValue); break; } }

Step 4: Register the Custom Element with the browser

To register our Custom Element with the browser, we need to pass it to customElements.define. I've placed this code into the main.ts:

// platformBrowserDynamic().bootstrapModule(AppModule) // .catch(err => console.error(err)); customElements.define('flight-info', FlightInfoElement);

Also, I've commented out the traditional code for bootstrapping the whole Angular application as I just want to provide a custom element. This will result in smaller bundle sizes.

Step 5: Call Custom Element/ Web Component

Now, we can try out our custom element/ web component. For this, just place the following code into your index.html:

<body> <flight-info id="info" flight='{ "id": 1, "from": "Here", "to": "There", "date": "now"}' selected="true"></flight-info> <script> const elm = document.getElementById('info'); elm.addEventListener('selected-change', e => console.debug('event', e)); </script> </body>

After starting the development web server (ng serve --aot -o), we should see our web component in action:

Web Component with flight information

Step 6: Compiling your web component

To get an optimized bundle for our consumers, we can now perform a production build:

ng build --prod

If configured right, the CLI will perform differential loading which means it generates ES5 bundles for legacy browsers (we all know of whom I'm speaking about) and ES2015 for modern ones:

Building the custom elements

To get one self-contained bundle instead of five for each compilation target, you can leverage my CLI extension ngx-build-plus:

ng add ngx-build-plus@^8.0.0-rc ng build --prod --single-bundle

Please make sure to get the right version of ngx-build-plus fitting to your CLI version. During RC phase, we'll need to go with the version ^8.0.0-rc as shown here, for instance.

Building our web component with ngx-build-plus looks like this:

Building with ngx-build-plus

More about ngx-build-plus and other options for bundling Angular Elements can be found in my article here.

As you can see, the ES2015 version of the resulted self contained bundle has just 58KB KB. If we compress it with brotli, just 18 KB are left as the extraction of the respective directory listening shows:

18.345 main-es2015.2b1d94d01898fb8c15d7.js.br 19.026 main-es5.ca289b744d789019b9ef.js.br

Further information

Using other components

To call other components, directives, or pipes, make sure they are connected via a module -- either directly or indirectly. In my case, I've put both, the wrapped FlightComponent and the called TitleComp into the same module:

@NgModule({ imports: [CommonModule], declarations: [FlightComponent, TitleComp] }) export class FlightModule { }

Using other services

Unfortunately, the approach outlined in the last section does not work for global services. However, you can use tree-shakable providers instead. As an alternative, you can also register them within @Component(..., providers: [...], ...) or you could pass an respective injector to renderComponent:

The following example shows an injector that registers the HttpClient and its dependencies:

import { Injector } from "@angular/core"; import { HttpHandler, HttpXhrBackend, HttpClient } from '@angular/common/http'; export const injector: Injector = Injector.create({ name: 'root', providers: [ { provide: HttpClient, deps: [HttpHandler] }, { provide: HttpHandler, useValue: new HttpXhrBackend({ build: () => new XMLHttpRequest() }) } ] });

To pass this injector to renderComponent, we use its parameter object:

ɵrenderComponent(FlightComponent, { host: this, injector });

As APIs like HttpClient increase your bundle size, I'd try to use native browser APIs instead for web components. We also have to keep in mind that Ivy cannot help much with such APIs: While it down levels UI-based code to code that is very close to the DOM, it cannot make our APIs disappear.

Not everything works yet

As we are using private APIs, we cannot expect everything to work. E. g. at the time of writing, Angular's FormsModule does not work together with renderComponent.

Conclusion

One big advantage of using Ivy is the relatively small bundle size we get although the web component bases upon Angular. Angular Elements for Ivy will automate some of the manual steps that have been performed here. Because of this and because of the usage of APIs that are still private, the solution presented is rather for early adopters who need to get started now.

 

 
Hier können Sie eine Anfrage für eine unverbindliche Schulung ode Beratung bzw. einen Workshop erstellen.
 
Unverbindliche Anfrage
 
 

Schulungen

Angular Schulung: Strukturierte Einführung

Lernen Sie in dieser interaktiven Schulung anhand einer Beispielanwendung den Einsatz von Angular für Ihre erfolgreichen Projekte kennen. Sie durchdringen die Hintergründe und bauen von der ersten Minute an auf Best Practices auf.

Details

Advanced Angular: Enterprise-Anwendungen und Architektur

In dieser Schulung erfahren Sie alles für die Entwicklung großer Anwendungen mit Angular: Mono-Repos, Micro-Apps, State Management, Performance und mehr

Details

Angular: Strukturierte Einführung

Seit der Ankündigung von Angular (2+) fragen sich Entwicklungs-Teams, welche Migrationspfade für AngularJS-1.x-Anwendungen vorliegen werden. Das im Lieferumfang von Angular enthaltene ngUpgrade bietet eine Antwort darauf. Es erlaubt einen Parallelbetrieb von AngularJS 1.x und Angular (2+) und stellt somit die Grundlage für eine schrittweise Migration dar.

Details

Progressive Web-Apps mit Angular

Progressive Web Apps bieten den Komfort nativer Anwendungen, indem sie auf moderne Browser APIs, wie Service Worker, setzen. Sie sind installierbar sowie offlinefähig und nutzen Hintergrundprozesse für Datensynchronisation und Push-Notifications. Diese Schulung zeigt anhand eines durchgehenden Beispiels was sich genau hinter diesem neuen Konzept verbirgt, wie solche Anwendungen mit Angular entwickelt werden und wie Sie in Ihren Projekten von den dahinterstehenden Ideen profitieren.

Details

Reaktive Architekturen mit Angular und Redux

Dieses interaktive Seminar vermittelt, wie Sie reaktive Anwendungen mit Angular entwickeln können.

Details

TypeScript

TypeScript gibt Ihnen alle Möglichkeiten der neuen JavaScript-Standards und zusätzlich ein statisches Typsystem, das dabei hilft, Fehler möglichst früh zu erkennen. Außerdem ist TypeScript die Grundlage für Angular. In diesem interaktiven Seminar lernen Sie diese mächtige Sprache anhand einer Fallstudie kennen.

Details

Weitere Schulungen ...