Getting Started with Angular Elements_870x220

Angular Elements solves the problem of code reuse across multiple frameworks and provides a way to use Angular components in non-Angular environments. Learn how you can start using them in your apps today.

Angular is awesome. It's a full-featured, robust framework with a lot of power under the hood. Wouldn't it be nice, though, to be able to use Angular in different contexts without all of the overhead? Maybe you need to share a custom Angular datepicker with other teams using other frameworks. Maybe you need to embed a tiny Angular application into a CMS. Or, maybe you'd like to use Angular components inside of your legacy AngularJS application as an alternative to the ngUpgrade library.

These are the problems that the Angular Elements library solves. Angular Elements is a project under Angular Labs, the Angular team's umbrella for new, experimental ideas. Angular Labs allows the team to break new ground without breaking your application. The first experimental release of Elements became part of Angular 6. It's a minimum viable product that's not yet ready for full-fledged production, but we should see a more refined version of Elements in Angular 7.

So, what exactly is the Angular Elements project and how do we start using it?

Web Components and Custom Elements

Before we dig into Angular Elements, we need to learn a little bit about web components.

Angular Elements lets you package your Angular components as custom web elements, which are part of the web components set of web platform APIs. Web components are technologies to help create reusable, encapsulated elements. Right now, that includes the shadow DOM, HTML templates, HTML imports, and custom elements. The custom elements technology powers Angular Elements.

Custom elements are reusable, encapsulated HTML tags to use in web pages and web apps. They can be used in any framework or library that uses HTML. In short, you're able to create custom DOM elements that have their own functionality that the rest of the page doesn't need to know anything about. (You can also modify existing DOM elements, though we won't be doing that with Angular Elements.)

To create a custom element, you simply need to create an ES6 class that extends from an HTML element and register that class with the browser through an API called the CustomElementRegistry. Custom elements have lifecycle hooks, such as the connectedCallback and the disconnectedCallback. They also allow you to register interest in attributes, which can also have callbacks and properties associated with them. And finally, like other DOM elements, custom elements can have events associated with them using addEventListener.

What is Angular Elements?

The Angular Elements library is essentially a bridge between Angular components and the custom web elements API. With Angular Elements, Angular components act as hosts for custom elements. Angular inputs map to properties, host bindings map to attributes, outputs map to events, and lifecycle hooks map to the custom element lifecycle hooks. As you can see, Angular was purposefully designed to interface easily with the DOM API, and custom elements are no exception.

Angular Elements also let us take advantage of Angular's dependency injection. When we create a custom element using Angular Elements, we pass in a reference to the current module's injector. This injector lets us share context across multiple elements or use shared services.

We also get content projection (transclusion) with Angular Elements, with a couple of caveats. Content project works correctly when the page first renders, but not with dynamic content projection yet. As of now, we also don't yet have support for ContentChild or ContentChildren queries. Content projection should get more robust over time, though, and we'll also have the ability to use slots and the shadow DOM as browser support increases.

Use Cases

If you used AngularJS (1.x) a lot, you may have appreciated how flexible it was. You could use AngularJS in lots of places: inside of a CMS, inside of a Microsoft MVC application, or as a full-fledged SPA (single-page application). Angular (2+) is a much more robust framework and was designed as a tool to build complete, full-featured applications. Angular Elements will restore flexibility to Angular, but with many more features than AngularJS could provide.

So, where would we use Angular Elements? Rob Wormald covered three main areas with examples in his ng-conf 2018 talk on Angular Elements:

Elements in Apps

  • CMS embeds
  • Dynamic components
  • Server-side/hybrid rendering

Element Containers

  • Mini-apps
  • Micro-frontends
  • ngUpgrade (a personal favorite)
  • SharePoint

Reusable Widgets

  • Cross-framework compatibility
  • Material/CDK components in any environment
  • Design systems - build once, use anywhere

Essentially, anywhere you think you may need Angular, you'll be able to use it with Angular Elements. The current release of Elements has been optimized for apps and containers, but there's still work to be done for reusable widgets. That brings us to our topic: the challenges facing Angular Elements today.

Hurdles to Production Use

While Angular Elements is looking extremely promising as of Angular 6, there are still a few challenges that must be overcome as the team gets it ready for production use:

  1. Bundle size: Currently, whether you ship a tiny widget or a full application as a custom element, a lot of Angular comes along with it. This makes the resulting bundle size of your custom element pretty large. Luckily, Ivy, the next generation of Angular's compiler, will solve this problem. Ivy is optimized for removing unused pieces of Angular and creating smaller bundles, so it's a match made in heaven for Angular Elements. You can learn more about Ivy in my article First Look: Angular Ivy.
  2. Steps to use: As you'll see in a moment when we build our first element, some of the setup and packaging of Angular Elements is a little clunky right now. This is to be expected from a first release, and I wouldn't sweat it. Based on how the Angular CLI has been developing over the last year, I'm sure this process will get ironed out. Ivy will also help with this, as support for and optimization of Angular Elements will be baked in from the start.
  3. Browser support: Browser support for custom elements is improving, but it's still not all the way there. Most browsers have at least experimental support of custom elements, but, as of this writing, custom elements are not yet supported in Internet Explorer or Edge. Unfortunately, this is an area outside of the Angular team's control. In the meantime, we can use polyfills to add browser support, though that does add to the setup clunkiness and the overall bundle size. Hopefully we won't have to use these polyfills for too much longer, but you can see this handy table of custom elements browser support to keep up to date with the status.

Getting Started with Angular Elements

Now that we know all about Angular Elements, let's create our own reusable custom element! We're going to make a simplified clone of the Momentum dashboard Chrome extension as a reusable component. It's straightforward, but we'll use an @Input, the *ngIf directive, and a template variable just to see Angular magic used out of context.

Just a reminder, Angular Elements is still in early days, so new updates might break things, and your development environment may affect some of these steps.

Angular Elements example

Create and Set-Up the Project

To get going, we're going to create a new Angular CLI project and add Elements to it.

First, make sure you have the Angular CLI installed globally (and be certain it is the latest version, at least 6.0.8 as of the time of this writing):

npm install -g @angular/cli

Let's create our application with the CLI. Once the install is done, navigate into the folder with cd and open it with your favorite editor (I'm using Visual Studio Code).

ng new momentum-element
cd momentum-element

We can add Angular Elements to our project using the ng add command and pass in the name of our project.

ng add @angular/elements --project=momentum-element

Not only does this command add Angular Elements, it also adds a polyfill called document-register-element and adds it to the scripts section of our Angular CLI config file (angular.json). This polyfill adds support for the custom elements API.

Create the Component

Now that we're set up, let's make our component. We'll just reuse our AppComponent that's generated by the CLI (it's inside /src/app). Replace the code in app.component.ts with the following:

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

@Component({
  selector: 'app-momentum-element',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  encapsulation: ViewEncapsulation.Native
})

export class AppComponent {
  @Input() name = 'friend';
  focus: string;
  focusSet = false;
  setFocus(value) {
    this.focus = value;
    this.focusSet = true;
  }
}

A couple of notes on this. First, the selector won't actually matter here, because we'll be registering this component as a custom element with its own tag. Second, notice that we're using ViewEncapulation.Native. Specifying an encapsulation strategy affects how our styles are applied to components. The default is called Emulated, which simulates the shadow DOM in order to have scoped styling, even in browsers that don't support the shadow DOM. We're flipping on the real shadow DOM here by using the Native strategy.

Other than that, we're just doing a couple simple things here. We're creating an @Input for the name attribute, which we'll default to "friend". We're also making a focus string property and a focusSet boolean property, as well as a function that sets that property and toggles the boolean to show and hide the input box.

Let's do the template next. Replace the contents of app.component.html with this:

<div class="widget-container">
  <div class="content">
    <h1>Hello, {{name}}.</h1>
    <input *ngIf="!focusSet" type="text" #userFocus (keydown.enter)="setFocus(userFocus.value)" placeholder="What's your focus today?" />
    <div *ngIf="focusSet" class="focus">
      <p>Your main focus today is:</p>
      <p>{{focus}}</p>
    </div>
  </div>
</div>

We've got a simple input with a template variable here so the user can type a focus for the day, hit enter, and it will display. Nothing too crazy, but we're taking advantage of Angular to make this easy.

Finally, let's add some style. Replace the contents of app.component.css with this:

.widget-container {
  color: white;
  font-family: arial;
  width: 400px;
  height: 300px;
  position: relative;
  background-image: url('https://source.unsplash.com/400x300?mountains,snow,high%20contrast');
}

.content {
  text-align: center;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

input {
  font-size: 24px;
  border: 2px black solid;
}

.focus {
  width: 300px;
  font-size: 20px;
}

That's all we need to do to get our component up and running.

Creating a Custom Element

So far, we haven't done anything related to Angular Elements here. In fact, you could update index.html to use app-momentum-element instead of app-root, run ng serve, and see the working component in the browser, just like normal.

To use our component as a reusable widget, we just need to modify the way our AppModule bootstraps. We only need to do two things to do this. First, instead of having the AppComponent in a bootstrap array, rename that array to entryComponents to prevent the component from bootstrapping with the module:

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  entryComponents: [AppComponent]
})

Next, we need to create the custom element. This can be done in a few different places, but we'll do it in a constructor function. You'll need to add Injector to the list of @angular/core imports, and you'll need to import createCustomElement from @angular/elements. Once that's done, add the constructor like this:

constructor(private injector: Injector) {
  const el = createCustomElement(AppComponent, { injector });
  customElements.define('momentum-element', el);
}

Here, we're calling the createCustomElement function, passing in our component and an instance of our module's injector (we're using ES6 destructuring here, but it's equivalent to {injector: injector}). This function returns a special class that we can use with the Custom Elements API, where we define our element by passing in a selector and the class.

Finally, we need to add ngDoBootstrap to override the bootstrap function. Our completed AppModule will look like this:

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

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  entryComponents: [AppComponent]
})

export class AppModule {
  constructor(private injector: Injector) {
  const el = createCustomElement(AppComponent, { injector });
  customElements.define('momentum-element', el);
}

ngDoBootstrap() {}
}

That's it!

Packaging Angular Elements

So far, we've created a component and turned it into a custom element. Now we need to package it up so that it can be used elsewhere with a script tag. This means we need to not only build the application as we normally would, but concatenate all of the script files that the CLI produces into a single file.

I'll go ahead and let you know, this is the roughest part of this process right now. There are several different ways you can accomplish this:

I'm going to show you the Node approach today, because it seems to work on multiple platforms without any issues. In the future, though, I'm sure there will be a CLI schematic for Angular Elements that generates a flat file structure and bundles into one file. Don't forget, we're only getting started with the potential for Elements!

To create a Node build script, you'll need to install two more dependencies:

npm install --save-dev concat fs-extra

Next, at the root of our project, create a file called elements-build.js and paste this in:

const fs = require('fs-extra');
const concat = require('concat');

(async function build() {
  const files = [
    './dist/momentum-element/runtime.js',
    './dist/momentum-element/polyfills.js',
    './dist/momentum-element/scripts.js',
    './dist/momentum-element/main.js'
  ];

  await fs.ensureDir('elements');
  await concat(files, 'elements/momentum-element.js');
  await fs.copyFile(
    './dist/momentum-element/styles.css',
    'elements/styles.css'
  );
})();

This script will take all of the scripts that the CLI generates and combine them into a single file. It will also move the CSS file over, though since we're using native encapsulation, this file will be empty.

Next, create a folder at the root of the project called elements. This is where we'll keep the concatenated files, as well as the HTML file we'll use to test our custom element.

Now, open up package.json and add a new script:

"build:elements": "ng build --prod --output-hashing none && node elements-build.js"

We're running the CLI build command with the prod flag and then running our build script with node.

Finally, in that new elements folder, create a file called index.html and paste this in:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Angular Elements</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <momentum-element name="Sam"></momentum-element>
    <script type="text/javascript" src="momentum-element.js"></script>
  </body>
</html>

Feel free to replace my name with yours in the name attribute!

That's all we need to do to get a working build of a custom element.

Our Custom Element in Action

Let's see if all this actually works. First, run our new build script:

npm run build:elements

You should see momentum-element.js and momentum-element.css in the elements folder alongside our HTML file. You'll see that the script file is about 200 kb right now, which is a bit large for something so simple. Don't worry, though - Ivy will help cut that down by quite a bit in the future.

Now let's serve up that folder. You'll want to install a simple HTTP server. I like to use static-server:

npm install -g static-server

You can then cd into that folder and start up the server:

cd elements
static-server

When you navigate to localhost:9080 (in a browser that supports custom elements, like Chrome), you should see your custom element!

Angular Elements example

We've got ourselves a fully functional custom element! Feel free to experiment and build on this example. You can add multiple instances of this element to do the DOM with different name inputs. You could also add custom event listeners to interact with other elements. Try dropping this custom element into a React or Vue app! You can check out my finished code at this GitHub repo.

Note: if you're getting an error like Failed to construct 'HTMLElement', you may have a package version problem. I was getting this error when I created a project with an older version of the CLI, even when I tried to manually update. Once I updated my global version of the CLI and generated the project, it worked. Of course, you'll also be unable to run this custom element in IE or Edge.

Conclusion

Hopefully you've begun to understand some of the power of Angular Elements. The flexibility of custom elements means there is a dizzying array of possibilities to use your Angular code across teams, frameworks, and even technologies. There are still some things to hammer out with the usage and tooling around Angular Elements, but this first release looks like a solid foundation for the next release!

To dig deeper into Angular Elements and custom elements in general, check out these resources:

Articles

Videos


Sam-Julien
About the Author

Sam Julien

Sam Julien is an Angular teacher and developer and the founder of the comprehensive video course UpgradingAngularJS.com. He's also the co-organizer of Angular Portland. When he's not coding, you'll find Sam outside hiking and camping like a good Oregonian.

Related Posts

Comments

Comments are disabled in preview mode.