Skip to content

Lit Signals

Use @wirestate/lit for context, decorators, and controllers. Use @wirestate/lit-signals for Lit Signals re-exports.

Service

ts
import { Inject, Injectable, WireScope } from "@wirestate/core";
import { signal, State } from "@wirestate/lit-signals";

@Injectable()
export class CounterService {
  public readonly count: State<number> = signal(0);

  public constructor(@Inject(WireScope) private readonly scope: WireScope) {}

  public increment(): void {
    this.count.set(this.count.get() + 1);
    this.scope.emitEvent("COUNTER_INCREMENTED", { count: this.count.get() });
  }
}

Root Provider

Create a root container on a Lit host.

ts
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ContainerProvider, containerProvide } from "@wirestate/lit";
import { CounterService } from "./CounterService";

@customElement("application-root")
export class ApplicationRoot extends LitElement {
  @containerProvide({
    config: {
      entries: [CounterService],
    },
  })
  private provider!: ContainerProvider;

  public render() {
    return html`<my-counter></my-counter>`;
  }
}

Managed Lit containers are created on connect and disposed on disconnect. Entries activate by default.

Injection

Child elements resolve services from the nearest container context.

ts
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { watch } from "@lit-labs/signals";
import { injection } from "@wirestate/lit";
import { CounterService } from "./CounterService";

@customElement("my-counter")
export class MyCounter extends LitElement {
  @injection(CounterService)
  private counter!: CounterService;

  public render() {
    return html` <button @click=${() => this.counter.increment()}>Count: ${watch(this.counter.count)}</button> `;
  }
}

Seeds

Pass startup data through provider config.

ts
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { ContainerProvider, containerProvide } from "@wirestate/lit";

@customElement("counter-root")
export class CounterRoot extends LitElement {
  @containerProvide({
    config: {
      entries: [CounterService],
      seeds: [[CounterService, { count: 100 }]],
    },
  })
  private provider!: ContainerProvider;
}

Read it in the service.

ts
import { Inject, Injectable, OnActivated, WireScope } from "@wirestate/core";
import { signal, State } from "@wirestate/lit-signals";

@Injectable()
export class CounterService {
  public readonly count: State<number> = signal(0);

  public constructor(@Inject(WireScope) private readonly scope: WireScope) {}

  @OnActivated()
  public onActivated(): void {
    const seed = this.scope.getSeed<{ count?: number }>(CounterService);

    if (typeof seed?.count === "number") {
      this.count.set(seed.count);
    }
  }
}

Events, Commands, Queries

Lit components can register handlers with decorators or controllers.

ts
import { Event } from "@wirestate/core";
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { injection, onCommand, onEvent, onQuery } from "@wirestate/lit";
import { CounterService } from "./CounterService";

@customElement("counter-tools")
export class CounterTools extends LitElement {
  @injection(CounterService)
  private counter!: CounterService;

  @onEvent("COUNTER_INCREMENTED")
  private onCounterIncremented(event: Event<{ count: number }>): void {
    console.log(event.payload?.count);
  }

  @onCommand("INCREMENT_COUNTER")
  private increment(): void {
    this.counter.increment();
  }

  @onQuery("COUNTER_LABEL")
  private label(): string {
    return "Counter";
  }
}

CounterTools owns the handlers. CounterPanel calls them through the same container scope.

ts
import { WireScope } from "@wirestate/core";
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { injection } from "@wirestate/lit";
import "./CounterTools";

@customElement("counter-panel")
export class CounterPanel extends LitElement {
  @injection(WireScope)
  private scope!: WireScope;

  @state()
  private label: string = "Unknown";

  private emitCounterEvent(): void {
    this.scope.emitEvent("COUNTER_INCREMENTED", { count: 0 }, this);
  }

  private incrementViaCommand(): void {
    void this.scope.executeCommand("INCREMENT_COUNTER").task;
  }

  private readLabel(): void {
    this.label = this.scope.queryData<string>("COUNTER_LABEL");
  }

  public render() {
    return html`
      <counter-tools></counter-tools>
      <span>${this.label}</span>
      <button @click=${() => this.emitCounterEvent()}>Emit event</button>
      <button @click=${() => this.incrementViaCommand()}>Run command</button>
      <button @click=${() => this.readLabel()}>Run query</button>
    `;
  }
}

Handlers follow the active container context. If a parent provider changes, Lit controllers unregister from the old bus and register on the new one.