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.
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 theHTMLElement
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 forNgElement
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.
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 ❤️.