MicroFrontend using Angular Elements


MicroFrontend is an interesting and much needed concept. Until recently, micro services were limited to the backend. The UI was a huge monolith, developed and maintained by the "UI folks". The fragility of the wide range of JavaScript frameworks and the delicate lunacies of the wide range of browsers made it even more difficult to develop and maintain the UI. On the other hand, the demands are ever increasing. You will find very few websites that were not revamped every year.

Micro Frontend is an important concept that lets us break this trend. With this, we can break the UI into smaller independent components that can be developed and managed independently. We can have part of the page implemented in Angular, part in React or Vue, and another part in simple vanila HTML.

Custom Elements


The primary enabler for MicroFrontend architecture is the concept of web components. Using this, we can create custom HTML elements. In the HTML code, a custom element is used like any other standard HTML element (e.g. h1, p, table...). But we can crunch a huge amount of functionality into these custom elements. With this in place, we can have different independent teams working on individual elements. The final index.html can be a trivial file that will rarely change.

These custom elements can be treated as any other HTML elements. We can use JavaScript functions on them; jQuery/CSS Selectors apply to them as they would apply to the common HTML elements. The only difference is that now we can define how the custom element works. We can stuff any amount of complexity into a custom element. It can communicate with the backend and invoke API's. It can communicate with other elements on the page - via vanila JavaScript. It can multiply or destroy itself without any external code.

Angular Elements


The Angular framework provides us the elements package that can help us with developing custom elements. Custom Elements is a JavaScript concept and independent of the Angular framework. But, the Angular Elements package provides some methods that help us do this job with less code.

In fact, custom elements are used all over the angular framework. We have all seen the index.html that is generated for an angular application. It is an innocent looking html file, with almost nothing in there. Just a tag "app-root" and a couple of JavaScript imports. What is this app-root? That is the tag that the Angular library looks out, and pushes all the generated HTML into it.

Now what is so special about this app-root? Nothing! We could have had anything else out there. If you check out the app.component.ts, the selector in the @Component.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

That is what defines the app-root. Try changing it to hello-world, and make the corresponding change in your index.html. Your code will run just fine! You just made a custom element - that can be used in any HTML, along with the JavaScript includes.

Of course, this trivial approach has a several limitations. We have an entire angular project dedicated to the given custom element. That may be fine. But, if we want to share some code between two different elements, it could get messy. If we create multiple custom elements using different angular projects, we end up in a namespace clash.

There are several such idiosyncrasies that need to be managed. The @angular/elements package helps us overcome several of these limitations.

@angular/elements


This is a utility package that implements a lot of functionality that we need to generate angular elements . Just install it using

ng add @angular/elements

This package has a function createCustomElement() - that provides a bridge from Angular's component interface and change detection functionality to the DOM API build in to the browser. Let us see how this can be used.

In a fresh, new Angular project, add the elements package, and then start with creating a new component

ng g c fun

As expected, it generates the required source files, and also updates the app.module.ts to include this new component. To create an angular element, we need to modify this file. Change it to look like this

app.module.ts


import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AppComponent } from './app.component';
import { FunComponent } from './fun/fun.component';

@NgModule({
  declarations: [
    AppComponent,
    FunComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  // bootstrap: [ AppComponent ],       - Remove this
  entryComponents: [ FunComponent ]
})
// Add code to the AppModule class.
export class AppModule { 
  constructor(injector: Injector) {
    customElements.define('krazy-fun', createCustomElement(FunComponent, {injector}));
  }

  ngDoBootstrap = () => {};
}

Apart from adding the appropriate imports, this is what we have done:

  • Removed the bootstrapping of the AppComponent. That is because, we are now focusing on the angular elements generated by the elements library and not the root element.
  • Define the FunComponent as a new entry component. We can have several entry components. The entry component is one that is directly included in the HTML code.
  • In the AppModule class, we added a new dummy method ngDoBootstrap. Since we removed the bootstrap array from the component, we need to override this method.
  • And we implement the constructor for the AppModule class. This is the most important code. createCustomElement - as the name suggests, creates a custom element out of the component that we pass in.We pass in the "injector" instance so that we can use dependency injection in the custom element. "customElements" is a JavaScript default object - that contains reference to all the custom elements to be interpreted by the browser. Essentially, this code generates a custom element out of the component and adds it to the customElements fun, with the tag as krazy-fun. This will be used in the HTML to refer to this custom element.

index.html


Now we can go ahead and change the HTML file to include this newly defined tag and remove the old app-root tag.

<krazy-fun></krazy-fun>

This works as expected. We can see the contents of the FunComponent on the screen.

Build


We have only seen the development environment so far. How do we deploy this in production? We will have to generate the dist package. Let's do that now

ng build --prod

This generates a big bunch of files. Six JavaScript files, a CSS file and the index.html, along with the assets and read me. This was not a problem when we hosted an entire web page. Now, it is a problem. Since we plan to ship this as a custom web element that someone would include into their web page. We cannot offer so many files. We need to crunch everything into something manageable. To do that, let's check what is generated by the build script.

$ ng build --prod
Generating ES5 bundles for differential loading...
ES5 bundle generation complete.

chunk {3} polyfills-es5.2a9b76947435a72ef5de.js (polyfills-es5) 141 kB [initial] [rendered]
chunk {2} polyfills-es2015.cdac51f3f194aeaa220f.js (polyfills) 49.2 kB [initial] [rendered]
chunk {0} runtime-es2015.0811dcefd377500b5b1a.js (runtime) 1.45 kB [entry] [rendered]
chunk {0} runtime-es5.0811dcefd377500b5b1a.js (runtime) 1.45 kB [entry] [rendered]
chunk {1} main-es2015.d124e2ba12debb0abd7b.js (main) 107 kB [initial] [rendered]
chunk {1} main-es5.d124e2ba12debb0abd7b.js (main) 130 kB [initial] [rendered]
chunk {4} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]
$

We can see two sets of JavaScript files. Three for es5 and three for es2015. (This differential loading is a common practice when we want to include the "lesser browsers"). In that spirit, we should share two JavaScript files, and a tag definition that can be included in the HTML code.

Also, if you check out the main-es2015*.js, you will see this in the first line:

(window.webpackJsonp=window.webpackJsonp||[]).push(.....

This defines the namespace of the JavaScript code that follows. Now this is the default namespace for any Angular code. So, anyone creating an angular element will have the same namespace. Naturally, you will clash. Hence, it is a good idea to change this namespace to something unique, so that there is no problem down the line.

There are elaborate angular packages that can help you do these things. But, I prefer to keep things simple. So I just use a bash script instead.

sed -i -e 's/webpackJsonp/krazymindsCustomElementJsonp/g' *.js
ls -r *es2015* | while read a; do cat $a >> krazyminds.customelement.es2015.js; rm $a; done
ls -r *es5* | while read a; do cat $a >> krazyminds.customelement.es5.js; rm $a; done

This gives us two JavaScript files that we can deliver on a CDN. They can be included in an HTML file as:

<script src="https://xxxxxxxxxxx/krazyminds.customelement.es2015.js" type="module"></script>
<script src="https://xxxxxxxxxxx/krazyminds.customelement.es5.js" nomodule defer></script>

Summary


Angular Elements let us generate custom elements that can be used stand-alone in any HTML code. They are independent of the framework and can be used within the browser, just as any other HTML element. It can cross framework boundaries and we can easily have an angular element on a website built with React.

One major drawback is that of network usage. Each element demands a huge amount of JavaScript code. If we have multiple angular elements on a single HTML, each will carry along its own JS code. Each of those will include the angular core - thus resulting in a lot of duplication.

But, with the onset of Ivy, everyone has high hopes on reduction of this JavaScript sizes.. thus reducing the network latency.