Angular
Angular adapter — provideLayerClient, signals, and imperative renderStack.
@stainless-code/angular-layers bridges stacks into Angular signals, rendered imperatively via renderStack(vcr). The core is re-exported.
Install
npm install @stainless-code/angular-layerspnpm add @stainless-code/angular-layersyarn add @stainless-code/angular-layersbun add @stainless-code/angular-layersPeer: @angular/core >=17.0.0
Client
Register LAYER_CLIENT with provideLayerClient() in your app config or component providers. Inject with useLayerClient():
import { provideLayerClient, useLayerClient } from "@stainless-code/angular-layers";
export const appConfig = {
providers: [provideLayerClient()],
};
Render a stack
There is no StackOutlet. Call renderStack(vcr) inside an injection context — it imperatively creates components in a ViewContainerRef, keyed by layer id so state changes update inputs via setInput without recreating instances:
import { Component, ViewContainerRef, inject } from "@angular/core";
import { renderStack } from "@stainless-code/angular-layers";
@Component({
selector: "app-root",
template: `<ng-container #outlet />`,
})
export class AppComponent {
private vcr = inject(ViewContainerRef);
constructor() {
renderStack(this.vcr, "confirm");
}
}
useStack({ stack, select }) returns a readonly Signal — the idiomatic equivalent of StackSubscribe.
Confirm dialog
import { Component, ViewContainerRef, inject } from "@angular/core";
import {
layerOptions,
provideLayerClient,
renderStack,
injectLayer,
type LayerComponentProps,
} from "@stainless-code/angular-layers";
type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;
@Component({
selector: "confirm-dialog",
template: `
<div role="dialog">
<h2>{{ payload.title }}</h2>
<button (click)="call.end(true)">Yes</button>
<button (click)="call.end(false)">No</button>
</div>
`,
})
class ConfirmDialogComponent {
call!: LayerComponentProps<ConfirmPayload, ConfirmResponse>["call"];
payload!: ConfirmPayload;
}
const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
stack: "confirm",
key: ["confirm", "remove"],
component: ConfirmDialogComponent,
});
@Component({
selector: "app-root",
providers: [provideLayerClient()],
template: `<button (click)="remove()">Remove</button><ng-container #outlet />`,
})
export class AppComponent {
private c = injectLayer(confirm);
private vcr = inject(ViewContainerRef);
constructor() {
renderStack(this.vcr, "confirm");
}
async remove() {
await this.c.open({ title: "Remove item?" });
}
}
Primitives
Options bag plus optional trailing LayerClient. Drive with injectLayer(options) (useLayer alias); observe with injectLayerState({ key, … }). Angular-idiomatic aliases: injectStack, injectQueuedStack, injectLayerQueuedState. Must run in an injection context.
| Export | Role |
|---|---|
LAYER_CLIENT |
InjectionToken<LayerClient> |
provideLayerClient(client?) |
FactoryProvider for LAYER_CLIENT |
useLayerClient() |
Inject the app client |
useStack({ stack?, select?, compare? }, client?) |
Readonly Signal<T> of the selected stack slice |
useQueuedStack({ stack?, select?, compare? }, client?) |
Queued snapshot signal |
injectLayer(options, client?) |
Drive — wired handle signals + open() / dismiss() + reactive state / queued / top |
injectLayerState({ key, stack?, select?, compare? }, client?) |
Observe — Signal<LayerState[]> for mounted same-key layers |
injectLayerQueuedState({ key, stack?, select?, compare? }, client?) |
Observe — queued same-key layers |
Ergonomic APIs
| Export | Role |
|---|---|
useStackHandles(stackId?, rootProps?) |
{ states: Signal, getCall } for headless rendering |
renderStack(vcr, stackId?, rootProps?) |
Imperative outlet — the Angular rendering surface |
useMutationFlow(call) |
pending is a Signal<boolean>; run(fn).orEnd(response) |
useLayerGroup(call, options?) |
Child stack with renderInto(vcr) instead of Outlet |
createStackHook(config?) |
{ provideClient, useAppStack, renderInto } |