Declare and inject front-end components
Last modified by Manuel Leduc on 2026/05/18 11:53
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 components
// 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
// 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 all components
// 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 and resolve
// 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();