Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

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-layers
pnpm add @stainless-code/angular-layers
yarn add @stainless-code/angular-layers
bun add @stainless-code/angular-layers

Peer: @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?) ObserveSignal<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 }

Learn more

Last updated on July 18, 2026

Was this page helpful?