Declare and inject front-end components

Last modified by Manuel Leduc on 2026/05/18 11:53

Content

Steps

  1. 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");
  2. 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>;
    }
  3. 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);
      }
    }
  4. 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)));
      }
    }
  5. 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)
      );
  6. 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();

Get Connected