Moving an application from AngularJS to Angular is often a tricky process. Framework-agnostic Web Components can smooth the way. With recent updates, Angular has gained the ability to expose Web Components, and starting from AngularJS 1.7.3, consuming them is straightforward.

Strictly speaking, Custom Elements is the more precise term; Web Components covers a broader set of standards. As noted at https://custom-elements-everywhere.com/, these integrate seamlessly with AngularJS:

AngularJS seamlessly works together w/ Custom Elements

This guide demonstrates a step-by-step migration path from AngularJS to Angular using this technique. New parts of the application are authored in Angular and shipped as Custom Elements, which the existing AngularJS shell then loads:

Sample for upgrading

The logos and dashed lines in the diagram highlight which sections belong to AngularJS and which are Angular.

The source code for this example is available in my GitHub repository.

Structure of the Project

A key benefit of this strategy is that AngularJS and Angular code remain fully separate. Each side is developed independently, and the integration happens at runtime when the bundle containing the Custom Elements is loaded.

Looking at the folder layout of the demo, you will see a dedicated directory for each framework:

Folder Structure

The Angular Side

Within the Angular folder, there is a component that renders a flight, which appears in the screenshot 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 registered as a Custom Element within 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); } }

Notice that the component is added to the module's entryComponents array in addition to being declared. This is necessary because Angular Elements creates instances of it dynamically at runtime.

During the application's bootstrap process, ngDoBootstrap is invoked. Inside it, createCustomElement transforms the component into a Custom Element, and customElements.define registers it with the browser.

In the example, the current injector is passed when wrapping the component, enabling dependency injection to work inside it. As Rob Wormald, the creator of Angular Elements, mentioned in his ngConf 2018 talk, this setup also allows multiple Web Components from the same application to share services and communicate with each other.

For cross-browser compatibility, the @webcomponents/custom-elements polyfill is included.

More details on using this polyfill are covered in my blog post series on Angular Elements.

A simple npm script, leveraging the cpr node package, copies the generated bundles over to the AngularJS app:

"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"

The AngularJS Side

The AngularJS application includes the AngularJS bundle alongside the bundles produced by the Angular project:

<!-- 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>

Since the polyfills and scripts bundles from the Angular side are also referenced, the required polyfills are loaded as well.

With that in place, the Custom Element can be used directly inside 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>

Pay attention to the new ng-prop-* and ng-on-* directives, introduced in AngularJS 1.7.3. These allow binding to any property or event on a (custom) element. Also, note that the property name selectedChange is written as selected_change in the template, following the naming convention of those directives.

Wrapping AngularJS Components as Custom Elements

So far, the focus has been on exposing Angular components as Custom Elements for use within AngularJS. But what about the reverse direction? Can an AngularJS component be turned into a Custom Element?

While there is no official solution yet, nothing stops you from manually wrapping AngularJS code. My demo for combining multiple technologies with Web Components uses exactly this technique:

Micro App

As explained in the accompanying article, that project is an Angular shell that loads Micro Apps as Web Components on demand. One of those Micro Apps is the AngularJS-based booking application shown above.

The complete source for the micro apps example is available here.

Final Thoughts and Assessment

With the strong support for Custom Elements from the Angular(JS) team, leveraging this modern approach for migration is quite simple: build the new Angular parts in a separate solution and integrate them into the existing AngularJS application via Custom Elements. Thanks to the recently added ng-prop-* and ng-on-* directives, using them from AngularJS templates is effortless.

Once every piece of the AngularJS application has been replaced with a Custom Element, the final switch to Angular is all that remains.

Compared to ngUpgrade, a notable advantage is that the two frameworks' codebases remain separate, which lowers complexity. However, ngUpgrade is the more capable option. It not only enables Angular components inside AngularJS apps but also the reverse, without manual wrapping. Furthermore, it facilitates sharing services in both directions.