Declare and inject front-end components
Version 2.1 by Manuel Leduc on 2026/05/18 11:49
Content
Steps
- Declare role symbols
Role identifiers are symbol values. Put them in a small, dependency-free module so every contributor can import them without pulling implementations.
// src/roles.ts export const NOTIFIER_ROLE = Symbol("Notifier"); export const CHANNEL_ROLE = Symbol("Channel"); export const FORMATTER_ROLE = Symbol("Formatter"); - Define interfaces
// src/types.ts export interface Formatter { format(message: string): string; } export interface Channel { send(message: string): Promise<void>; } export interface Notifier { notify(message: string): Promise<void>; } - Implement leaf components (no dependencies)
Each implementation class is marked @injectable().
// src/plain-formatter.ts import { injectable } from "@xwiki/platform-component-annotation-default"; import type { Formatter } from "./types"; @injectable() export class PlainFormatter implements Formatter { format(message: string): string { return message; } }// src/loud-formatter.ts import { injectable } from "@xwiki/platform-component-annotation-default"; import type { Formatter } from "./types"; @injectable() export class LoudFormatter implements Formatter { format(message: string): string { return message.toUpperCase() + "!"; } }// src/console-channel.ts import { injectable } from "@xwiki/platform-component-annotation-default"; import type { Channel } from "./types"; @injectable() export class ConsoleChannel implements Channel { async send(message: string): Promise<void> { console.log(`[console] ${message}`); } }// src/memory-channel.ts import { injectable } from "@xwiki/platform-component-annotation-default"; import type { Channel } from "./types"; @injectable() export class MemoryChannel implements Channel { public readonly sent: string[] = []; async send(message: string): Promise<void> { this.sent.push(message); } } - Implement a component that injects others
DefaultNotifier depends on:
- a specific Formatter selected by name (@inject + @named),
- every registered Channel (@injectAll).
// src/default-notifier.ts import { inject, injectAll, injectable, named, } from "@xwiki/platform-component-annotation-default"; import { CHANNEL_ROLE, FORMATTER_ROLE } from "./roles"; import type { Channel, Formatter, Notifier } from "./types"; @injectable() export class DefaultNotifier implements Notifier { constructor(@inject(FORMATTER_ROLE) @named("loud") private readonly formatter: Formatter, @injectAll(CHANNEL_ROLE) private readonly channels: Channel[], ) {} async notify(message: string): Promise<void> { const formatted = this.formatter.format(message); await Promise.all(this.channels.map((c) => c.send(formatted))); } } - Register every contribution
Registrations attach lazy loaders to the shared manager. The loaders are only invoked on first resolution, so this file stays cheap to import.
// src/register.ts import { manager } from "@xwiki/platform-component-manager-default"; import { CHANNEL_ROLE, FORMATTER_ROLE, NOTIFIER_ROLE } from "./roles"; manager .registerComponent( FORMATTER_ROLE, () => import("./plain-formatter").then((m) => m.PlainFormatter), { name: "plain" } ) .registerComponent( FORMATTER_ROLE, () => import("./loud-formatter").then((m) => m.LoudFormatter), { name: "loud" } ) .registerComponent( CHANNEL_ROLE, () => import("./console-channel").then((m) => m.ConsoleChannel), { name: "console" } ) .registerComponent( CHANNEL_ROLE, () => import("./memory-channel").then((m) => m.MemoryChannel), { name: "memory" } ) .registerComponent( NOTIFIER_ROLE, () => import("./default-notifier").then((m) => m.DefaultNotifier) ); - Initialize, then resolve
Bootstrap code calls _init() once, after all registrations have been performed. Application code then awaits resolverPromise.
// src/bootstrap.ts import "./register"; // side-effect import: triggers registrations import { _init, resolverPromise } from "@xwiki/platform-component-manager-default"; import { NOTIFIER_ROLE } from "./roles"; import type { Notifier } from "./types"; async function main() { await _init(); const resolver = await resolverPromise; const notifier = await resolver.getAsync<Notifier>(NOTIFIER_ROLE); await notifier.notify("hello world"); // → ConsoleChannel logs: "[console] HELLO WORLD!" // → MemoryChannel appends "HELLO WORLD!" to its `sent` array. } main();