Skip to content

@platform/sdk-angular

The official, optional Angular adapter for @platform/sdk — thin wrappers over the core’s public API, built on Angular’s DI and signals. Install it only on Angular:

Terminal window
npm install @platform/sdk @platform/sdk-angular

It declares @angular/core: >=21 as a peerDependency, resolved against your app’s own Angular. The core works without it — see Framework adapters.

edit-body (writer) + edit-row (reader)
import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core";
import { defineStateChannel } from "@platform/sdk";
import {
activateHost,
bindWriter,
injectSurfaceConnection,
PLATFORM,
stateSignal,
} from "@platform/sdk-angular";
interface Draft {
id: string;
name: string;
}
// One channel definition, shared by the writer (the modal body) and the reader (the parent list).
const draftChannel = (id: string) => defineStateChannel<Draft>(`draft:${id}`);
// A modal body / surface. injectSurfaceConnection gives typed params; activateHost self-sizes it
// (called synchronously in the constructor — an injection context); bindWriter publishes the live draft
// so the parent frame can mirror it.
@Component({
selector: "edit-body",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<input [value]="name()" (input)="name.set(read($event))" />`,
})
export class EditBody {
private readonly conn = injectSurfaceConnection<Draft>();
protected readonly name = signal(this.conn.params.name);
constructor() {
activateHost(this.conn);
bindWriter(draftChannel(this.conn.params.id), this.conn.state, () => ({
id: this.conn.params.id,
name: this.name(),
}));
}
protected read(e: Event): string {
return (e.target as HTMLInputElement).value;
}
}
// The parent list reflects the modal's live draft via a read-only Signal (unsubscribed on destroy).
@Component({
selector: "edit-row",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<b>{{ draft()?.name }}</b>`,
})
export class EditRow {
private readonly platform = inject(PLATFORM);
protected readonly draft = stateSignal(draftChannel("c_1"), this.platform.state);
}
  • PLATFORMInjectionToken<Platform> for the connected top-level client. Provide it in your bootstrap: { provide: PLATFORM, useValue: platform }.
  • SURFACE_CONNInjectionToken<SurfaceConnection> for a surface or modal body’s connection.

injectSurfaceConnection<P>(): TypedSurfaceConnection<P>

Section titled “injectSurfaceConnection<P>(): TypedSurfaceConnection<P>”

Inject the SURFACE_CONN connection with params narrowed to P, dropping the per-component as P cast. TypedSurfaceConnection<P> is a SurfaceConnection with readonly params: P.

bindWriter<S>(channel, state, producer): StateWriter<S>

Section titled “bindWriter<S>(channel, state, producer): StateWriter<S>”

Publish a reactive snapshot to a cross-frame StateChannel: the returned StateWriter is seeded with producer(), then an effect pushes every later producer(); cleanup is bound to the owner’s DestroyRef. Call it from an injection context.

stateSignal<S>(channel, state): Signal<S | undefined>

Section titled “stateSignal<S>(channel, state): Signal<S | undefined>”

The reader counterpart to bindWriter: mirror a cross-frame channel into a read-only Signal, seeded from the channel’s current snapshot, updated on every publish, and unsubscribed via DestroyRef. Call it from an injection context.

Self-size a surface or modal body: after the next render it observes the component’s host element and signals ready one frame later (so custom elements have upgraded and the first reported height is the final one). Call it synchronously in the component constructor (an injection context); it injects ElementRef to find the host element.