Create and use Custom Elements with Angular

In this article, we'll take a look at the new custom elements feature in Angular6. We will see how to create a custom element as well as how to reuse it in a simple application.

Web Components

Before we get into the details, let's spend a little time on discussion web components. Web components are what allows us to create custom elements - elements which are encapsulated, have their functionality and can be reused in every web app.

Web components have three parts, which are:

  • Custom elements: a JavaScript API that allows us to define custom elements and their behaviour
  • Shadow DOM: Enables the encapsulation of the element - the styles and scripts added to our element will be kept "private" and won't interfere with other parts of our document/application
  • HTML templates: Using the templates we can create reusable code. The markup placed between template elements is not going to be visible but can be referenced by JavaScript and appended to the DOM.

One interesting fact about the Shadow DOM is that you have probably used it before even without realising it. The HTML <video> element uses the Shadow DOM - it places the controls (if the attribute is specified) which adds a whole bunch of items to the shadow-root.

Angular Elements

With the release of Angular6, we can also create custom elements via this package called Angular Elements. It's something that we need to install separately by executing npm i @angular/elements (or we can also add it via ng add @angular/elements.

Within this package, we get access to a createCustomElement() function that essentially allows us to create a custom element from any Angular component. This may sound way too easy, and the truth is, it really is. We can now transform any component to a custom element with ease.

We can think about these custom elements as Angular components, but without any Angular knowledge, terms and usage conventions. So when we include them in an application we don't need to bring any Angular specific items - we can use them as standalone items.

Creating the application

Let's first walk through the creation of a custom element. As with any Angular project, we'll start by creating a new project via the Angular CLI: ng new custom-element.

Once the dependencies have been installed, cd into the folder and add the @angular/elements dependency by executing ng add @angular/elements.

Next, let's create a new component that we'll use: ng g c greeter.

This component is going to be simple (I do apologise for the lack of creativity here, but I merely wish to show how to create a custom element without having to spend a lot of time explaining the component itself). It will take a name attribute and produce a greeting.

In light of the above, this is how the component template should look like:

<span>Hi, it's nice to meet you {{ name }}! 👋</span>

And the corresponding TypeScript component should look like this:

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

@Component({
  selector: 'app-greeter',
  templateUrl: './greeter.component.html',
  styleUrls: ['./greeter.component.css']
})
export class GreeterComponent implements OnInit {
  @Input()
  public name: string;
  
  constructor() { }

  ngOnInit() {
  }

}

Test the application

We can now go ahead and test this application by adding the following content to app.component.html:

<app-greeter name="John">

Run ng serve and open the browser to see the component in action. The result should be as seen in the screenshot below:

Creating the custom element

It's now time to transform our component to a custom element. There are a bunch of things that we need to do.

First and foremost, we need to make some changes to app.module.ts:

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

import { AppComponent } from './app.component';
import { GreeterComponent } from './greeter/greeter.component';

import { createCustomElement } from '@angular/elements';

@NgModule({
  declarations: [
    AppComponent,
    GreeterComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  entryComponents: [ GreeterComponent ]
})
export class AppModule {
  constructor(private injector: Injector) { }

  ngDoBootstrap() {
    const myCustomElement = createCustomElement(GreeterComponent, { injector: this.injector });
    customElements.define('app-greeter', myCustomElement);
  }
}

We have included createCustomElement from @angular/elements as well as Injector from @angular/core. Notice we have removed the bootstrap property completely from the module definition and added the entryComponents property.

Last but not least we have also added some code the AppModule class - the most important part here is the ngDoBootstrap() method which is responsible for creating the custom element when the Angular application bootstraps.

The createCustomElement() function takes two parameters - the first one is the Angular component that we want to 'elementise' and the second is a configuration object, which in our case is the current Injector instance.

The second part in the ngDoBoostrap() code is to define the custom elements. Note that this is not related to Angular, the customElements is a property of the Window interface and it's a reference to the CustomElementRegistry object, which does have the define() method which, as its name suggests, allows us to define a new custom element.

Now it's time to create the actual element. We can do this by building the Angular project - this can be done by executing the following command: ng build --prod --output-hashing none.

If you are wondering what is --output-hashing none do - it simply removes the hashes from the names of the generated JavaScript files. We are doing this because later on, we'll concatenate all these files together and there's no need for the hash values in the filenames.

The result of the build command should be a dist folder with a bunch of JavaScript files as well as an index.html file. The custom element is now ready however at the time of writing this article the current release of Angular doesn't offer a build functionality to create only Angular Elements - i.e. pure custom elements. But we can create a separate build process that would allow us to do this.

Let's go ahead and add gulp to our project: npm i gulp gulp-concat and add a gulpfile.js with the following content:

const gulp = require('gulp');
const concat = require('gulp-concat');

gulp.task('concat', function() {
  return gulp.src([
    './dist/custom-element/runtime.js',
    './dist/custom-element/polyfills.js',
    './dist/custom-element/scripts.js',
    './dist/custom-element/main.js',
  ])
    .pipe(concat('app-greeter.js', { newLine: ';' }))
    .pipe(gulp.dest('./dist/'));
});

gulp.task('default', ['concat']);

As we can see the above will concatenate all the JavaScript files into one file. Let's run gulp by executing node_modules/gulp/bin/gulp.js. This should generate an app-greeter.js file in the dist folder.

Now we can try to use this custom element.

Let's move app-greeter.js somewhere else and let's create an index.html file as well with at a bare minimum the following content:

<script src="app-greeter.js"></script>
<app-greeter name="Tamas"></app-counter>

Serve this HTML file - either via http-server or Python's Simple HTTP server, it really doesn't matter - the result should be that you see the same as if you'd be running the application as an Angular app.

The resulting JavaScript file (app-greeter.js) is 244k in size which is considerable. With future releases of Angular, it is expected that there will not only be CLI support to build custom elements but also because of the new rendering engine (Ivy) will help improve the sizing.

Conclusion

Building custom elements are straight-forward with Angular, even though at the moment there are some manual things that we need to do. Mostly at the moment, custom elements are supposed to be used inside Angular6 applications and going forward (maybe in Angular7? ) support for elements will be increased and there will be much more support for them, not only in Angular applications but outside of them as well.