Since Angular 14.2, it\'s possible to use Standalone Components as Angular Elements. In this article, I\'m going to show you, how this new feature works.
Providing a Standalone Component
The Standalone Component I\'m going to use here is a simple Toggle Button called ToggleComponent
:
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-toggle',
standalone: true,
imports: [],
template: `
<div class="toggle" [class.active]="active" (click)="toggle()">
<slot>Toggle!</slot>
</div>
`,
styles: [`
.toggle {
padding:10px;
border: solid black 1px;
cursor: pointer;
display: inline
}
.active {
background-color: lightsteelblue;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class ToggleComponent {
@Input() active = false;
@Output() change = new EventEmitter<boolean>();
toggle(): void {
this.active = !this.active;
this.change.emit(this.active);
}
}
By setting encapsulation to ViewEncapsulation.ShadowDom
, I\'m making the browser to use "real" Shadow DOM instead of Angular\'s emulated counterpart. However, this also means that we have to use the Browser\'s slot
API for content projection instead of Angular\'s ng-content
.
Installing Angular Elements
While Angular Elements is directly provided by the Angular team, the CLI doesn\'t install it. Hence, we need to do this by hand:
npm i @angular/elements
In former days, @angular/elements
also supported ng add
. This support came with a schematic for adding a needed polyfill. However, meanwhile, all browsers supported by Angular can deal with Web Components natively. Hence, there is no need for such a polyfill anymore and so the support for ng add
was already removed some versions ago.
Bootstrapping with Angular Elements
Now, let\'s bootstrap our application and expose the ToggleComponent
as a Web Component (Custom Element) with Angular Elements. For this, we can use the function createApplication
added with Angular 14.2:
// main.ts
import { createCustomElement } from '@angular/elements';
import { createApplication } from '@angular/platform-browser';
import { ToggleComponent } from './app/toggle/toggle.component';
(async () => {
const app = await createApplication({
providers: [
/* your global providers here */
],
});
const toogleElement = createCustomElement(ToggleComponent, {
injector: app.injector,
});
customElements.define('my-toggle', toogleElement);
})();
We could pass an array with providers to createApplication
. This allows to provide services like the HttpClient
via the application\'s root scope. In general, this option is needed when we want to configure these providers, e. g. with a forRoot
method or a provideXYZ
function. In all other cases, it\'s preferable to just go with tree-shakable providers (providedIn: 'root'
).
The result of createApplication
is a new ApplicationRef
. We can pass it\'s Injector alongside the ToggleComponent
to createCustomElement
. The result is a custom element that can be registered with the browser using customElements.define
.
Please note that the current API does not allow for setting an own zone instance like the noop
zone. Instead, the Angular team wants to concentrate on new features for zone-less change detection in the future.
Side Note: Bootstrapping Multiple Components
The API shown also allows to create several custom elements:
const element1 = createCustomElement(ThisComponent, {
injector: app.injector,
});
const element2 = createCustomElement(ThatComponent, {
injector: app.injector,
});
Besides working with custom elements, the ApplicationRef
at hand also allows for bootstrapping several components as Angular applications:
app.injector.get(NgZone).run(() => {
app.bootstrap(ToggleComponent, 'my-a');
app.bootstrap(ToggleComponent, 'my-b');
});
When bootstrapping a component this way, one can overwrite the selector to use. Please note, that one has to call bootstrap
within a zone in order to get change detection.
Bootstrapping several components was originally done by placing several components in your AppModule
\'s bootstrap
array. The bootstrapApplication
function used for bootstrapping Standalone Components does, however, not allow for this as the goal was to provide a simple API for the most common use case.
Calling an Angular Element
To call our Angular Element, we just need to place a respective tag in our index.html
:
<h1>Standalone Angular Element Demo</h1>
<my-toggle id="myToggle">Click me!</my-toggle>
As a custom element is threaded by the browser as a normal DOM node, we can use traditional DOM calls to set up events and to assign values to properties:
<script>
const myToggle = document.getElementById('myToggle');
myToggle.addEventListener('change', (event) => {
console.log('active', event.detail);
});
setTimeout(() => {
myToggle.active = true;
}, 3000);
</script>
Calling a Web Component in an Angular Component
If we call a web component within an Angular component, we can directly data bind to it using brackets for properties and parenthesis for events. This works regardless whether the web component was created with Angular or not.
To demonstrate this, let\'s assume we have the following AppComponent:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<h2>Root Component</h2>
<my-toggle
[active]="active"
(change)="change($event)">
Hello!
</my-toggle>
`,
})
export class AppComponent {
active = false;
change(event: Event) {
const customEvent = event as CustomEvent<boolean>;
console.log('active', customEvent.detail);
}
}
This Standalone Component calls our my-toggle
web component. While the Angular compiler is aware of all possible Angular components, it doesn\'t know about web components. Hence, it would throw an error when seeing the my-toggle
tag. To avoid this, we need to register the CUSTOM_ELEMENTS_SCHEMA
schema.
Before, we did this with all the NgModules we wanted to use together with Web Components. Now, we can directly register this schema with Standalone Components. Technically, this just disables the compiler checks regarding possible tag names. This is binary - the checks are either on or off -- and there is no way to directly tell the compiler about the available web components.
To make this component appear on our page, we need to bootstrap it:
// main.ts
[...]
// Register web components ...
[...]
app.injector.get(NgZone).run(() => {
app.bootstrap(AppComponent);
});
Also, we need to add an element for AppComponent to the index.html:
<app-root></app-root>
Bonus: Compiling Self-contained Bundle
Now, let\'s assume, we only provide a custom element and don\'t bootstrap our AppComponent
. In order to use this custom element in other applications, we need to compile it into a self contained bundle. While the default webpack-based builder emits several bundles, e. g. a main bundle and a runtime bundle, the new -- still experimental -- esbuild-based one just gives us one bundle for our source code and another one for the polyfills.
To activate it, adjust your project settings in your angular.json
as follows:
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
[...]
}
Normally, you just have to add -esbuild
at the end of the default builder.
The resulting bundles look like this:
favicon.ico (948 bytes)
index.html (703 bytes)
main.43BPAPVS.js (100 177 bytes)
polyfills.M7XCYQVG.js (33 916 bytes)
styles.VFXLKGBH.css (0 bytes)
If you use your web component in an other web site, e. g. a CMS-driven one, just reference the main bundle there and add a respective tag. Also, reference the polyfills. However, when using several such bundles, you have to make sure, you only load the polyfills once.
The last listing also shows a tradeoff of Angular Elements: Parts of Angular end up in the bundle. Hence, we have an overhead of some KB per bundle.
What\'s next? More on Architecture!
Standalone Components are quite interesting for our architectures. However, there are also other topics we need to considern:
- According to which criteria can we subdivide a huge application into sub-domains?
- How can we make sure, the solution is maintainable for years or even decades?
- Which options from Micro Frontends are provided by Module Federation?
Our free eBook (about 120 pages) covers all these questions and more:
Feel free to download it here now!