Upgrading with Web Components: From AngularJS to Angular

Upgrading from AngularJS to Angular can be challenging. Framework-independent Web Components can help here. Fortunately, Angular now allows exposing Web Components and beginning with AngularJS 1.7.3 you can easily consume them.

To be more precise, we should use the word Custom Elements instead of Web Components as Web Components is an umbrella term for several standards. According to https://custom-elements-everywhere.com/, they really work well with AngularJS:

AngularJS seamlessly works together w/ Custom Elements

In this post, I show how to use this for migrating an AngularJS solution step by step to Angular. For this, I write new application parts with Angular and provide them as Custom Elements which are loaded into the AngularJS context:

Sample for upgrading

You can tell by the logos and the dashed lines where the AngularJS and where the Angular parts are.

The source code can be found in my GitHub account.

Project Structure

The advantage of this approach is, that one doesn't need to intermingle AngularJS and Angular code. Instead, both parts of the application are written separately and the integration happens at runtime when a bundle with Custom elements is loaded.

Hence, when you look at the folder structure of my example, you will find an own folder for each of those parts:

Folder Structure

Angular Part

The Angular folder contains this component displaying a flight as shown above:

@Component({ templateUrl: './flight-card.component.html' }) export class FlightCardComponent implements OnChanges { @Input() item: any; @Input() selected: boolean; @Output() selectedChange = new EventEmitter<boolean>(); deactivate(): void { // this.selected = false; this.selectedChange.emit(false); } activate(): void { // this.selected = true; this.selectedChange.emit(true); } }

This component is exposed as an external web component in its module:

import { createCustomElement } from '@angular/elements'; [...] @NgModule({ imports: [ BrowserModule ], declarations: [ FlightCardComponent ], bootstrap: [], entryComponents: [ FlightCardComponent ] }) export class AppModule { constructor(private injector: Injector) { } ngDoBootstrap() { const flightCardCE = createCustomElement(FlightCardComponent, { injector: this.injector }); customElements.define('flight-card', flightCardCE); } }

As you see here, the component is not only declared but also put into the module's entryComponents section. This is because Angular Element instantiates it dynamically at runtime.

In ngDoBootstrap which is called during bootstrapping the Angular application, createCustomElement wraps it as a Custom Element and customElements.define registers it with the browser.

As you see in this example, when wrapping the component the current injector is passed. Hence, the component can make use of dependency injection. As Angular Element's father, Rob Wormald, pointed out in his talk at ngConf 2018, this also allows different web components provided by the same application to communicate with each other using services.

To make sure it works with every browser, I'm using the the polyfill @webcomponents/custom-elements.

Further information about using this polyfill, you can find in my blog article series regarding Angular Elements.

To provide the AngularJS application with the bundles, the example uses a simple npm script copying it over using the cpr nodejs package:

"build": "npm run bundle:ce && npm run copy:ce",
"copy:ce": "cpr dist/demo ../angularjs/angular-ce -d",
"bundle:ce": "ng build --prod --output-hashing none"

AngularJS part

The AngularJS application references both, the AngularJS bundle as well as the bundles from the Angular part:

<!-- Bundles for Angular part --> <script type="text/javascript" src="angular-ce/runtime.js"></script> <script type="text/javascript" src="angular-ce/polyfills.js"></script> <script type="text/javascript" src="angular-ce/scripts.js"></script> <script type="text/javascript" src="angular-ce/main.js"></script> <!-- Bundles for AngularJS part --> <script src="main.js"></script>

As it also references the polyfills bundle and the scripts bundle from the Angular part, we get the mentioned polyfills too.

After this, you can use the Custom Element within the AngularJS templates:

<div ng-repeat="b in $ctrl.bookings"> <flight-card ng-prop-item="b" ng-prop-selected="$ctrl.selection[b.id]" ng-on-selected_change="$ctrl.change(b, $event)"></flight-card> </div>

Please have a look at the new directives ng-prop-* and ng-on-* which have been introduced with AngularJS 1.7.3. They allow to bind any property and any event of an (custom) element. Please also note, that the name selectedChange becomes selected_change here which is due to a convention of those directives.

Exposing AngularJS components as Custom Elements

Ok, so far we've seen how to expose an Angular Element as an Custom Element which can be used in an AngularJS application. But what's with the other way round? Is is also possible to wrap an AngularJS component as an Custom Element?

Well, while there is not an official solution for this, no one can prevent you from manually wrapping AngularJS stuff. I'm using this technique in my showcase for combinding several technologies by means of Web Components:

Micro App

As described in my article about this, this example consists of an Angular application -- the shell -- which loads Micro Apps provided as Web Components on demand. One of those Micro Apps is the AngularJS based booking app shown above.

The full source code for this micro apps example can be found here.

Conclusion and Evaluation

Thanks to the Angular(JS) team's great support for Custom Elements, using this modern technique for migrating from AngularJS to Angular is straight forward: Just write the Angular parts in an new Angular solution and integrate them into the existing AngularJS counterpart by leveraging Custom Elements. Due to the newly introduced ng-prop-* and ng-on-* calling them within AngularJS is just a piece of cake.

After every building block of the AngularJS part has been replaced by such a Custom Element, you can fully switch to Angular.

One advantage over using ngUpgrade is that the Angular and the AngularJS parts are not intermingled which reduces complexity. However, ngUpgrade on the other side is more powerful. It not only allows using Angular components within AngularJS applications but also using AngularJS components within Angular applications without the need to wrap them by hand. Also, it allows mutual access to services on both sides.