Introduction#

I have been working with Adobe Experience Manager (AEM) for almost two years and most of the time I combined it with Angular as a frontend framework. One of my first tasks as an AEM developer was to find a good way to use Angular in conjunction with the CMS platform.

At the time, Angular 6 had just been released and one of its main new features was Angular Elements. Angular Elements allows to create Web Components from Angular components and reuse them in other applications using different technologies. Since Web Components are framework agnostic and self-bootstrapped they are great for dynamic sites with CMS platforms.

There are some articles you can read about Angular Elements:


Building#

One of the good things of Web Components and custom elements is that you import some JS and CSS and you are ready to use them. With Angular you can run ng build --prod and import the generated bundle files in other applications to use your custom elements.

Also you can use ngx-build-plus to build your custom elements. With this library you can get a single bundle file when building your project with Angular CLI.


The problem#

Despite there are use cases where it could be handy to have all the components bundled to a single or a few files -like design systems-, there are other cases where it is not ideal.

In my particular case I have an Angular project with about 20 -big- components that are included as custom elements in a dynamic site powered by Adobe Experience Manager. But, only one or two of those components are included in each page.

So, if only one of the components is used within a page I would be delivering a lot of unnecessary JavaScript to the browser, leading to slower loading times.


Lazy loading#

Code splitting and lazy loading would help to tackle that problem. You can split your application into multiple NgModules accordingly.

In my case, I could split up my project by creating a separate NgModule for each of my components and one or more shared modules to share features across the whole project. Now I would only need to lazy load them in order to lazy load my components.

There are several options to lazy load components in Angular, for example:

But, how to lazy load the components from Non-Angular applications?


ngx-element#

With ngx-element you can lazy load your Angular components from everywhere. That means from a CMS platform, a React application or just a plain HTML.

The library will define a custom element which you can pass a selector attribute to. That selector attribute determines what component you want to load. Also you can pass in attributes to your component by setting data-attributes to the custom element.


Usage#

Let’s create a small Angular application to see ngx-element in action :) I am using Angular CLI v9.0.6. Choose SCSS as the css preprocessor.

$ ng new lazy-components --minimal
$ cd lazy-components

We can remove app.component.ts since we won’t need it and modify app.module.ts accordingly.

After doing that our app.module.ts file should look like this:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

@NgModule({
  declarations: [],
  imports: [
    BrowserModule
  ],
  providers: []
})
export class AppModule {
  ngDoBootstrap() {}
}

Basically I removed the App component and added the ngDoBootstrap method since we are not bootsrapping any component in the module.

Now let’s create a Talk component together with its feature module.

$ ng g module talk
$ ng g component talk

At the moment you should have the following folder structure:

Folder structure with a feature module and a component.

And your talk files should look as follow:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TalkComponent } from './talk.component';

@NgModule({
  declarations: [TalkComponent],
  imports: [
    CommonModule
  ]
})
export class TalkModule { }

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-talk',
  template: `
    <p>
      talk works!
    </p>
  `,
  styles: []
})
export class TalkComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

Let’s change our Talk component to make it display some information about a talk in a conference and give it some styles.

Update the talk.component.ts file to the following:

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-talk',
  templateUrl: './talk.component.html',
  styleUrls: ['./talk.component.scss']
})
export class TalkComponent implements OnInit {

  @Input() title: string;
  @Input() description: string;
  @Input() speaker: string;
  @Input() tags: string;

  talkTags: string[];

  constructor() { }

  ngOnInit() {
    this.talkTags = this.tags ? this.tags.split(',') : [];
  }

}

And create the following talk.component.html and talk.component.scss files next to talk.component.ts:

<div class="talk">
    <h2 class="title">{{ title }}</h2>
    <p class="description">{{ description }}</p>

    <div class="foot">
        <div class="tags">
            <span class="badge" *ngFor="let tag of talkTags">{{ tag }}</span>
        </div>
        <p class="speaker">{{ speaker }}</p>
    </div>
</div>
.talk {
    background-color: #f0f0f0;
    margin-top: 20px;
    padding: 10px 20px;
    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
    transition: box-shadow .3s ease;

    &:hover {
        box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
    }

    .title {
        margin-bottom: 0;
    }

    .description {
        margin-top: 0;
    }

    .foot {
        display: flex;
        justify-content: space-between;
        margin-top: 20px;

        .speaker {
            margin: 5px 0;
        }

        .badge:first-child {
            margin-left: 0;
        }
    }
}

Until now we have created a component that (trust me) will look like this later:

So nothing strange until now, right? We have created a typical Angular application with an AppModule, a feature module and one component.

Remember that our goal is to use this component in Non-Angular applications and be able to lazy load it. We need Angular Elements and ngx-element in order to do that, so let’s put them in action…

Install Angular Elements

Angular provides a schematic to install and set up Angular Elements in our project. It will add a polyfill but it does not support IE11. If you need IE11 don’t use this schematic and see this article instead.

$ ng add @angular/elements

Install ngx-element

$ npm install ngx-element --save

Expose the Talk component for ngx-element

In order to let ngx-element to access our component and create it on demand we need to make a couple of additions to our talk.module.ts.

First we need to add TalkComponent to the entryComponents array. And second we are going to add a customElementComponent property to the module in order to make the component’s class accessible to ngx-element.

Our talk.module.ts should be like this now:

import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TalkComponent } from './talk.component';

@NgModule({
  declarations: [TalkComponent],
  imports: [
    CommonModule
  ],
  entryComponents: [TalkComponent]
})
export class TalkModule {
  customElementComponent: Type<any> = TalkComponent;
}

Once we have done this we need to import and configure the NgxElementModule in our AppModule as follows:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgxElementModule } from 'ngx-element';

const lazyConfig = [
  {
    selector: 'talk',
    loadChildren: () => import('./talk/talk.module').then(m => m.TalkModule)
  }
];

@NgModule({
  declarations: [],
  imports: [
    BrowserModule,
    NgxElementModule.forRoot(lazyConfig)
  ],
  providers: []
})
export class AppModule {
  ngDoBootstrap() {}
}

Let’s test our component! 🎉

In order to test our component we are going to create some HTML where we can use it. Remember that we are not bootstrapping any Angular component and we are just adding custom elements to the DOM.

Replace the index.html file in the project with the following markup:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>LazyComponents</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
  </head>
  <body>
    <div class="content">
      <h1 class="section-title">Talks</h1>
      <div class="talks">
        <ngx-element
          selector="talk"
          data-title="Angular Elements"
          data-description="How to write Angular and get Web Components"
          data-speaker="Bruno"
          data-tags="Angular,Elements"></ngx-element>

        <ngx-element
          selector="talk"
          data-title="Lazy loading"
          data-description="How to lazy load Angular components in non-angular applications"
          data-speaker="Somebody else"
          data-tags="Angular,Lazy loading,Components"></ngx-element>
      </div>
    </div>
  </body>
</html>

And replace the global styles.scss file with:

@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');

body {
    font-family: 'Open Sans';

    .content {
        max-width: 1000px;
        margin: auto;

        .section-title {
            margin-top: 60px;
        }

        .sponsors {
            display: flex;
            flex-wrap: wrap;

            sponsor-element {
                margin: 0 20px;
            
                &:first-child {
                    margin-left: 0;
                }
            }
        }
    }
}

.badge {
    margin: 0 5px;
    padding: 5px 10px;
    background-color: #000099;
    color: white;
    border-radius: 15px;
    font-size: 11px;
}

Run it! 🚀#

At this point, if you run ng serve you should see our component in action:

And you can see that our Talk Module is being lazy loaded as we expected.


Play with it#

Now you can open your DevTools in the Network tab and verify that our TalkModule is being lazy loaded.

Some things you can play with to see the powers of custom elements:

  • Add a new talk to the DOM and see how it is self-bootstrapped.
  • Change the title , description and speaker attributes from the DevTools.
  • Remove the talk custom elements from index.html file and verify that the TalkModule is not loaded initially. Then add a talk element to the DOM on the fly from the DevTools and verify that the TalkModule is lazy loaded.

Conclusion#

With ngx-element we have built a component and leveraged all benefits of Angular framework, custom elements and lazy loading.

This library has changed the way I integrate Angular and Adobe Experience Manager for the better. I hope this can be useful for developers trying to use Angular as a frontend framework together with CMS platforms or any other Non-Angular projects.