Transform an Angular Component into a native Custom Element

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 2 years ago

Dynamic components aren’t always that easy, especially if you’d like to retrieve and instantiate them as part of an HTML coming from the server API. In this lesson we’re going to transform an Angular component into an Angular Element and dynamically insert it into the HTML by using the innerHTML property of a DOM element.

Note, if you get an error "Failed to construct 'HTMLElement': Please use the 'new' operator..", make sure to install the @webcomponents/webcomponentsjs polyfill and add the following '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; at the end of polyfills.ts. I discuss this as well in the lesson.

Also check out this GitHub issue for more infos.

Instructor: [00:00] Here we have a simple greeter component which does nothing else than printing out, "Hi there." I then reference this one here in my app component, as you can see here, by using the doGreet tag. That's why we see here in our browser "Hi there" printed out.

[00:14] Assume we want to insert this tag in a much more dynamic fashion. Let's create here a simple container, which is nothing else than a div. Let's also create a button. Whenever I click that button, I want to insert that greeter tag. Let's create here a method, addGreeter, inside our app component.

[00:34] Here, we simulate that dynamic insertion by simply leveraging some browser APIs. I am having here an instance of my container by using the document.getElementById. On that container, I am using the innerHTML. I'm inserting here my doGreet tag.

[00:52] Let's try this out and click the button. You can see actually nothing happens. Let's inspect our elements tab here. We see our container elements. We also see the doGreet tag being inserted properly. Our code actually works.

[01:06] The problem is that Angular doesn't recognize the doGreet tag because it has been inserted into the template in a dynamic fashion. Right now, Angular needs all of the elements to be present in the template or to use the dynamic component factory to instantiate it at runtime via your code.

[01:21] The whole flexibility Angular Elements gives us is actually to do something like this by inserting a tag dynamically. Then Angular will take care of instantiating that component on the fly.

[01:32] This can be very useful when, for instance, you have a server-side CMS system which gives you back already a pre-rendered HTML piece which you want to just insert into your whole document.

[01:42] Let's see how to leverage Angular Elements to achieve just that. First of all, we need to install the Angular Elements. We can do that by the ng add command.

[01:56] Once the installation is done, we can go to the package.json and inspect what has been inserted by the Angular add command. Here, we see the Angular Elements library, which has been added to our package.json. You might have different versions here, depending on the Angular version you are currently using. Also inserted here a document-register-element polyfill.

[02:16] If we go to the angular.json file, we should see there a similar script being registered for our app which we are starting. Once we have that installed, let's try and instantiate our Angular Elements. If we go to the app module, we see there the app component and greeter component being registered.

[02:34] Whenever we insert something dynamically, we also have to pass it to the entry components. Let's add here our greeter component. Furthermore, we can leverage the Angular Elements API to actually create an Angular element out of our greeter component.

[02:49] In the constructor of our app module, we create here an element. Then we import the createCustomElement function from the Angular Elements package we just installed. Now, we can directly use that below here.

[03:05] We pass here the type of our greeter component, as well as we have to give it the injector which it will use internally for resolving the dependencies via the dependencies injector.

[03:14] In order to be able to pass it in, we need to also get it here in our constructor. We can say something like "injector = injector." We auto-import that here from angular/core. We can just pass it along like this.

[03:29] With that, we have created here an instance of a custom element out of our Angular component. Now we use the Custom Elements API, which is a native browser API to actually define its tag. Let's call it doGreet and give it here the element.

[03:45] What we can also do since we define the tag here, in order to prevent people from using this component directly because now it should be used via the Custom Element API, we can comment here the selector tag.

[03:58] Let's go back. Let's save also our app module. Let's restart here our application. Let's go to the browser and have a refresh there as well. If we click, we see still nothing happens. There are some errors here in the console. Let's inspect them.

[04:17] If you get such an error such as here, "Please use the new operator. This DOM object constructor cannot be called as a function," then you might run into a problem of the polyfills. As we have seen, Angular Elements installs here that document-register-element polyfill.

[04:35] There are various ways to circumvent this problem. First of all, we can go to the tsconfig file. Rather than having as a target ES5, we can target ES2015 in case we are running evergreen browsers.

[04:48] Let's restart here our compilation process. Once it's up and running, let's click that button. We can see now it gets inserted properly. Our custom element here gets instantiated. We also see that this doGreet tag now has a version attached to it, which means that this is a separate Angular application if you want inside the existing Angular app.

[05:06] Since compiling as a target to ES2015 might not be an option yet, what you can do alternatively is to install an auto-polyfill. Let's stop here our compilation process. Let's add here the Web Components webcomponentsjs polyfill. I'm installing it with yarn. You can do as well as an npm install.

[05:27] Once installation finishes, we go to our polyfills file, which is inside our project. At the very end here, we import the Web Components webcomponentsjs/custom-elements-es5-adapter.js.

[05:41] Let's save this. Let's restart the Angular CLI. Once the compilation succeeds, let's refresh again our browser. Now if we reclick that addGreeter, we see again this works just as we expect. Also, if we go down to our elements panel, we can open our container. We see our doGreet component has been instantiated properly.

Wilgert Velinga
Wilgert Velinga
~ 2 years ago

Should we really instantiate the el in the constructor? Or is it actually better to do that in a lifecycle hook like ngOnInit?

Juri Strumpflohner
Juri Strumpflohnerinstructor
~ 2 years ago

@Wilgert Usually that’s fine, yes. I often also use the ngDoBootstrap hook. Basically as in the example of the video, rather than placing the code in the constructor, create a method in the Module class body called ngDoBootstrap and move the logic there.

export class AppModule {
  constructor(private injector: Injector) {}

  ngDoBootstrap() {
    const ngElement = createCustomElement(GreeterComponent, {
      injector: this.injector
    customElements.define('do-greet', ngElement);
~ 2 years ago

I get an error on intellisense: "Argument of type 'ngElementConstructor<unknown> is not assignable to parameter of type 'CustomElementConstructor'. ts(2345)