A Practical Guide to Angular Elements

Published on April 7, 20186 min read

Learn about the final changes of Angular Elements concerning Angular v6 and how to install, create and use an embeddable Custom Element using Angular Elements.

Preface

In my recent article about Angular Elements – _ Building a Custom Element Using Angular Elements"_, we introduced with an experimental Angular Labs project – Angular Elements and explained the motivation for this project. For demonstration purposes only, we developed an Angular component that displays a customizable "Made With Love" message and exposed it as a custom element using Angular Elements. In order to finish the demonstration, we used this custom element in a React project.

I advise you to go over my recent article (if you haven’t read it yet) – it’ll give you a sense of the concept in general.

Angular Elements project has been released officially!
Angular Elements project has been released officially!

This article is going to focus on the practical side of Angular Elements, considering the final changes that were made and compared to the experimental project which was examined as part of Angular Labs.

How to Install

The sixth version of Angular includes a new scoped package which’s named @angular/elements.

Just as a regular npm package, we install it using our favorite package manager:

npm install @angular/elements

This package provides the necessary tools for creating a custom element based on an Angular component.

In addition, Custom Elements v1 aren’t fully supported yet by the browsers – so we need to install a polyfill:

npm install @webcomponents/custom-elements

Of course, we should import it inside the polyfills.ts file:

// Used for browsers with partially native support of Custom Elements
import '@webcomponents/custom-elements/src/native-shim';

// Used for browsers without a native support of Custom Elements
import '@webcomponents/custom-elements/custom-elements.min';

Introducing the Package

The @angular/elements package has a responsibility of making out of your Angular component a proper custom element, which’s ready to be inserted into the CustomElementRegistry object. If you’re not familiar with the CustomElementRegistry object – it’s an object that’s used to register new custom elements and retrieve information about previously registered custom elements.

This package arrives with essential exported classes and functions which make the bridging (attributes, events and life-cycle hooks) between custom elements and Angular to be really easy.

Let’s take a look at the basics:

  • NgElement – an abstract class which extends the HTMLElement class and obligates to implement the necessary life-cycle hooks of a custom element.
  • createCustomElement – a function which takes a component class and an optional configuration for the created class, such as, setting up the initial injector. This function creates and returns an implementation for NgElement based on the provided component.

Here’s the signature of the createCustomElement function:

function createCustomElement<P>(component: Type<any>, config: NgElementConfig): NgElementConstructor<P> {

Now we’re going to apply these basics on the “Made With Love” component we’ve already built.

Defining a Custom Element

Let’s inspect the component we developed:

@Component({
  selector: 'made-with-love',
  template: `
    <ng-template #noUrl>
      {{ name }}
    </ng-template>
    <span [style.font-size.em]="size">
      Made with <span [style.color]="color">♥</span> by
      <ng-container *ngIf="url && url.length > 0; else noUrl">
        <a [attr.href]="url" target="_blank">{{ name }}</a>
      </ng-container>
    </span>
  `
  styleUrls: ['./made-with-love.component.scss']
})
export class MadeWithLoveComponent implements OnInit {
  @Input()
  public name: string;

  @Input()
  public url: string;

  @Input()
  public color: string = 'red';

  @Input()
  public size: number = 1;

  ngOnInit() {
    if (!this.name || this.name.length === 0) {
      console.error(`Name attribute must be provided!`);
    }
  }
}

Obviously, the piece of code above demonstrates a regular Angular component and there’s nothing special about it (here’s an explanation of that component).

Now, we register that component in the declarations and entryComponents arrays of the AppModule:

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

import { MadeWithLoveComponent } from './made-with-love/made-with-love.component';

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

Remember that Custom Elements (Angular Elements in particular) are self-bootstrapping – which means these are automatically started when they are added to the DOM and automatically destroyed when removed from the DOM. Hence, we don’t register a bootstrap component with this module and we don’t need more than an empty and manual invoking of ngDoBootstrap.

Note: entryComponents is an array of components which aren’t embedded in a particular template, but still created somehow imperatively.

To produce a custom element of the component we’ve developed – we should invoke createCustomElement. However, the registerAsCustomElements function (which we introduced in the Labs project) was renamed to createCustomElement and unlike the previous one – the fresh function doesn’t insert the created custom element into CustomElementRegistry – what means we’re responsible to take care of this job (hopefully that will happen automatically in the future).

Let’s use the constructor of AppModule to define our custom element:

export class AppModule {
  constructor(private injector: Injector) {
    const customElement = createCustomElement(MadeWithLoveComponent, { injector });
    customElements.define('made-with-love', customElement);
  }

  ngDoBootstrap() { }
}

Well, we take the customElement that was created by createCustomElement and bind it to a suitable selector. The define method is what that registers this custom element in CustomElementRegistry. Notice any registered custom element is accessible by the customElements array (a read-only property of Window).

The final project so far is attached here:

Using a Custom Element

One of the main objectives of Angular Elements is to allow us to create reusable and embeddable components not just for the Angular community – but for everyone.

We can easily embed the custom element that we just made inside any internal HTML file of the project – assuming that the needed scripts (including polyfills) are imported correctly.

Let’s take the index.html file as example and use our custom element:

<made-with-love
  name="Nitay Neeman"
  url="http://nitayneeman.com"
  size="2"
></made-with-love>

All we do is to attach the line above inside the index.html file.

Here’s how it seems:

This particular case is pretty straightforward. We embed a custom element inside an Angular application with the appropriate polyfills. In fact, that’s the main objective which the core team has placed for Angular v6.

But, what happens when it comes to embedding the custom element inside an external HTML file, which’s out of our project? 🤔

Officially, Angular v6 doesn’t support standalone publication for Angular Elements so that these aren’t bundled and shippable yet in a way they could be consumed by any type of application. Don’t worry, this is only the first phase of Angular Elements! Absolutely, the best is yet to come, apparently with Angular v7 (when Ivy will land officially).

Ivy is a new backwards-compatible Angular renderer focused on further speed improvements, size reduction, and increased flexibility.

Although that standalone publication isn’t supported yet for Angular Elements, we could definitely take care of this job. It means we might bundle a subset of Angular’s core with our custom element, including polyfills. Of course, this bundle should be published somewhere.

Actually, I’ve created a project that demonstrates this process. In a nutshell, it takes the same component we’re investigating during this article and generates the distribution files using ng build --prod. These output files are concatenated into a single file using gulp and published into the official registry of npm.

Generating concatenated files for scripts and styles using gulp
Generating concatenated files for scripts and styles using gulp

Notice that the concatenated .js file is about 300KB. That’s rather a lot for a component which just displays a simple message. However, we should also remember that Angular Elements still have a level of dependency on Angular under the hood – along with some polyfills which either increase the bundle size.

Furthermore, there is another issue here regarding version conflicts of Angular. We know that styles in the Shadow DOM might be encapsulated and protected from the parent document – but that’s not the case for scripts. In case we use our custom element in another Angular project (for instance, an Angular v4 application) – it could be problematic to mix it with a different version of Angular (which arrives with the custom element) on top of the same application.

All of these drawbacks will be probably improved when:

  • Ivy will land and reduce Angular size dramatically (by bundling what really matters).
  • Ivy will either make multiple versions of Angular to be unnecessary (by reducing the level of dependency on Angular’s core in terms of Custom Elements).
  • Bundling scripts process will be more automatic with Ivy and Angular CLI in Angular v7.
  • The browsers will have a native support for Custom Elements v1 – so these polyfills become unnecessary.

Conclusions

We learned today how to create an embeddable custom element using Angular Elements.

These are important key points to remember:

  • The @angular/elements package arrives with a basic exported function for creating Custom Elements from Angular components.
  • Polyfills for Custom Elements v1 are still necessary for most of the browsers.
  • We should take care of inserting the NgElement into CustomElementRegistry.
  • Angular Elements of v6 are aimed at using inside Angular applications.
  • Angular Elements of v7 will be more standalone and embeddable inside any external applications.
  • Embedding Angular Elements of v6 inside external applications is possible but requires a manual and suitable bundling process.
  • Ivy will land officially as part of Angular v7.
  • Ivy is going to be a key player regarding bundle size and version conflicts of Angular.

Angular core team, thank you very much for this awesome project – I’m sure you made it with ❤️.

Follow Me

Join My Newsletter

Get updates and insights directly to your inbox.

Site Navigation


© 2024, Nitay Neeman. All rights reserved.

Licensed under CC BY 4.0. Sharing and adapting this work is permitted only with proper attribution.