04 · Angular Elements & Micro-Frontends¶
Sometimes an Angular component needs to live somewhere that isn't an Angular app: a CMS
page, a server-rendered site, a React app owned by another team, a static marketing page.
Angular Elements (@angular/elements) packages a component as a custom element —
a browser-native HTML tag — so any page can use it with plain HTML and DOM APIs.
This lesson turns the reading list's StarRating component (Level 1, lesson 10) into a
<star-rating> element, drives it from vanilla JavaScript, and then discusses the larger
topic of micro-frontends.
Building an element¶
Bootstrap an application without a root component and register the element:
import { createApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { provideBrowserGlobalErrorListeners } from '@angular/core';
import { StarRating } from './app/reading/star-rating';
createApplication({ providers: [provideBrowserGlobalErrorListeners()] })
.then((app) => {
const element = createCustomElement(StarRating, { injector: app.injector });
customElements.define('star-rating', element);
})
.catch((err) => console.error(err));
createApplication gives you an environment injector with your providers (HTTP,
services, …) but renders nothing by itself. createCustomElement wraps the component in
a class the browser understands. The component itself is unchanged.
ng build produced a single initial bundle of 127.62 kB raw (38.14 kB estimated
transfer) — smaller than the full reading-list app because there's no router or other
pages.
Using it from plain HTML¶
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Plain HTML page</title>
<base href="/">
</head>
<body>
<h1>Not an Angular page</h1>
<p>Rate this article: <star-rating id="r" value="2" label="Article rating"></star-rating></p>
<p id="out">(no rating yet)</p>
<script>
// Plain DOM code — no Angular here.
const r = document.getElementById('r');
r.addEventListener('valueChange', (e) => {
document.getElementById('out').textContent = 'You rated ' + e.detail;
});
</script>
</body>
</html>
We served the build and drove the element from JavaScript in Chromium:
1 initial (value="2") 2 stars on | aria-label: Article rating
2 clicked 4th star 4 on | "You rated 4"
3 r.value = 1 (property) 1 on | property reads 1
4 r.setAttribute('value','5') 5 on | typeof r.value: string, "5"
5 created with createElement 5 buttons, label "Second"
console errors: none
The mapping from Angular to DOM:
| Angular | Custom element |
|---|---|
input() / model() named label |
attribute label and property label (camelCase inputs map to dash-case attributes: maxValue → max-value) |
output() / model() change named valueChange |
DOM CustomEvent valueChange, payload in event.detail |
| component creation | when the element is connected to the document |
| component destruction | shortly after the element is disconnected |
The type trap¶
Look at row 4. Attributes are always strings, so setAttribute('value', '5') put the
string "5" into a component that thinks value is a number. It happened to render
because star <= value() coerces, but value() + 1 would give "51". For elements,
add input transforms (numberAttribute, booleanAttribute — Level 1, lesson 06) to
every non-string input, or document that consumers must set properties, not attributes.
Other things to know¶
- Styles: the element uses the component's view encapsulation. We checked
r.shadowRoot—falsewith the default emulated encapsulation. UseViewEncapsulation.ShadowDomif the host page's CSS must not leak in (and accept that your styles can't be themed from outside except through CSS custom properties). - Zoneless works: inputs set from outside are applied through the same signal-input
machinery as
setInput, so the element re-renders without Zone.js. - Size: each separately built element bundle includes its own copy of Angular.
Putting several elements in one build (define them all in
main.ts) shares the framework code between them.
Micro-frontends¶
"Micro-frontends" means splitting one product's front end into parts that different teams build and deploy independently, composed in the browser. The motivation is organisational — team autonomy and release independence — not technical elegance. Common approaches:
- Custom elements (this lesson). Framework-agnostic, simple contract (attributes, properties, events). Each part ships its own framework copy unless you arrange sharing.
- Module / Native Federation. A host app loads remote bundles at runtime and shares
dependencies (such as
@angular/core) between them via a manifest. For Angular's esbuild-based builder this is provided by community projects (Native Federation is the commonly used one); check its compatibility with your Angular version before adopting. - Route-level composition by the server or edge: different paths served by different apps, sharing only a design system and authentication.
Honest trade-offs:
- Independent deployment means runtime integration: version mismatches, duplicated dependencies, and contracts you can no longer check with the compiler.
- Shared state, routing and design consistency all become explicit problems.
- A well-structured single app (lesson 06) or a monorepo with libraries (lesson 02) gives most teams the separation they need with far less complexity. Reach for micro-frontends when independent release cycles are a hard requirement.
How It Actually Works¶
createCustomElement generates a class extending HTMLElement and registers
observedAttributes derived from the component's inputs. The browser calls:
connectedCallback→ Angular creates the component withcreateComponentusing your injector, attaches its host view to the application, and applies any attributes or properties set earlier (properties set before upgrade are captured and replayed).attributeChangedCallback→ the attribute string is passed to the matching input (through its transform, if any) — hence the string in row 4.- property setters → forwarded to the input directly, keeping the original type.
disconnectedCallback→ Angular schedules destruction; if the element is re-attached quickly (e.g. moved in the DOM), destruction is cancelled.
Outputs are subscribed once the component exists, and each emission is re-dispatched as a
CustomEvent on the host element. Because the component is attached to a real
ApplicationRef, change detection, DI and signals behave exactly as in an Angular app.
Common mistakes¶
- Number and boolean inputs without transforms (strings from attributes).
- Several independently built elements on one page, each shipping Angular.
- Relying on host-page CSS to style an element with Shadow DOM encapsulation.
- Choosing micro-frontends for technical reasons rather than organisational ones.
- Forgetting cleanup in components that start timers or subscriptions — elements are destroyed when removed from the DOM, and leaks multiply on pages that add and remove them often.
Exercise¶
- Turn one of your components into a custom element and use it in a static HTML file with no build step on the page itself.
- Add
numberAttributeto its numeric inputs and repeat thesetAttributeexperiment: confirm the property is now a number. - Listen for one of its outputs from vanilla JavaScript and log
event.detail. - Use the same element inside a small React or Vue page (or a plain
<script type="module">) and note what integration work was needed. - Write a one-page decision record: for your organisation, would micro-frontends solve a real problem that a monorepo wouldn't?