Skip to content
Skillv1.0.0

angular-layers

Angular adapter for @stainless-code/angular-layers (signals + DI + imperative renderStack); use when wiring provideLayerClient, renderStack/useStackHandles, useLayerGroup, useMutationFlow, or createSt

by stainless-code(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from stainless-code/layers (packages/angular/skills/angular-layers/SKILL.md). Install upstream with npx skills add stainless-code/layers --skill angular-layers. Copyright stays with the author (MIT).

Angular layer/stack UI with @stainless-code/angular-layers

Open any layer from anywhere in Angular and manage modal, dialog, drawer, popover, and toast UI as ordered, named stacks. Awaiting a typed result and fire-and-forget invocation (void client.open(...)) are equally first-class; named stacks, singletons via upsert with live update, serial queues, nested child stacks, transitions, dismissal blockers, and validation also provide standalone value.

The package re-exports @stainless-code/layers, so adapter and core APIs share one import path. It builds compiler-free with tsdown — ergonomic wrappers (renderStack, useStackHandles, useLayerGroup, useMutationFlow, createStackHook) use an imperative rendering model instead of adapter-shipped outlet components. See the layers core skill, architecture, and repository README.

When to use this skill

Reach for @stainless-code/angular-layers to open overlay UI imperatively from anywhere and manage ordered, named stacks instead of prop-drilling, lifting state, or threading callbacks.

It fits when you need any of:

  • Open or close overlay UI from anywhere.
  • Await a typed result or fire and forget with void client.open(...); both are first-class.
  • Named stacks, singletons via upsert with live update, or serial queues.
  • Nested stacks with useLayerGroup, transitions, dismissal guards, or validation.

Skip it only for a single always-local overlay with no return, stacking, queue, animation, or guard needs and no wish for a global registry.

Full fit matrix: When to use Layers.

Install

bun add @stainless-code/angular-layers

Core is included. @angular/core (>=17.0.0, signals) is a required peer; install it separately if the app does not provide it.

Declare → Mount → Call

// 1. Declare — register a component on the layer
import { Component, Input } from "@angular/core";
import {
  layerOptions,
  type LayerComponentProps,
} from "@stainless-code/angular-layers";

export type ConfirmPayload = { title: string };
export type ConfirmResponse = boolean;

@Component({
  selector: "app-confirm-dialog",
  standalone: true,
  template: `
    <div role="dialog">
      <h2>{{ payload.title }}</h2>
      <button type="button" (click)="call.end(true)">Yes</button>
      <button type="button" (click)="call.end(false)">No</button>
    </div>
  `,
})
export class ConfirmDialogComponent {
  @Input({ required: true })
  call!: LayerComponentProps<ConfirmPayload, ConfirmResponse>["call"];
  @Input({ required: true }) payload!: ConfirmPayload;
}

export const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialogComponent,
  exitingDelay: 200,
});

@Component({
  selector: "app-saved-toast",
  standalone: true,
  template: `<div role="status">Saved</div>`,
})
export class SavedToastComponent {
  @Input({ required: true }) call!: LayerComponentProps["call"];
}

export const savedToast = layerOptions<void>({
  stack: "toast",
  key: ["toast", "saved"],
  component: SavedToastComponent,
});
// 2. Provide one client + imperatively mount the stack
import type { ApplicationConfig, FactoryProvider } from "@angular/core";
import { Component, ViewContainerRef } from "@angular/core";
import {
  LAYER_CLIENT,
  LayerClient,
  provideLayerClient,
  renderStack,
} from "@stainless-code/angular-layers";

const provider: FactoryProvider = provideLayerClient();

export const appConfig: ApplicationConfig = {
  providers: [provider],
};

@Component({
  selector: "app-confirm-host",
  standalone: true,
  template: "",
})
export class ConfirmHostComponent {
  constructor(vcr: ViewContainerRef) {
    renderStack(vcr, "confirm"); // injection context required
  }
}

// Equivalent custom registration:
const customProvider: FactoryProvider = {
  provide: LAYER_CLIENT,
  useFactory: () => new LayerClient(),
};

renderStack(vcr, stack?, rootProps?) creates each layer's registered component via ViewContainerRef.createComponent + setInput, keyed by stable layer id so state changes update inputs without remounting. injectLayer, injectLayerState, useStack, renderStack, useStackHandles, useLayerGroup, and useMutationFlow must run in an Angular injection context — typically a component constructor or field initializer. Outside a constructor, use runInInjectionContext(injector, () => …).

// 3. Call & await — injectLayer binds identity; open takes payload only
import { Component } from "@angular/core";
import { injectLayer, useLayerClient } from "@stainless-code/angular-layers";
import { confirm, savedToast, type ConfirmResponse } from "./confirm.layer";

@Component({
  selector: "app-opener",
  standalone: true,
  template: `
    <button type="button" (click)="open()">Remove</button>
    <button type="button" (click)="showSaved()">Save</button>
  `,
})
export class OpenerComponent {
  private readonly c = injectLayer(confirm);
  private readonly client = useLayerClient();

  async open() {
    const ok: ConfirmResponse = await this.c.open({ title: "Remove?" });
    // ok: boolean
  }

  showSaved() {
    void this.client.open(savedToast);
  }
}

When P is void, unknown, undefined, or a union containing undefined, omit the payload key; R defaults to void. Accordingly, savedToast opens fire-and-forget with neither await nor a payload key.

The call context

Each mounted layer gets a LayerCallContext from createCallContext(stack, layer, state) (or via useStackHandles().getCall(state) / renderStack's call input). It provides end, dismiss, update, setRunning, settle, ended, index, stackSize, root, stackId, layerId, and addBlocker; the LayerState snapshot provides payload, data, error, phase, transition, actionStatus, and dismissing. Use await call.end(response) to resolve the caller's await and dismiss the layer (Promise<boolean>; false when a blocker vetoes). When undefined extends R (e.g. void toasts), omit the arg — call.end() / call.dismiss().

Key vs id: key is the logical identity used by find, upsert, and gcTime; each mount gets a unique instance id. Track s.id in @for because parallel stacks may contain multiple layers with the same key.

Wired handle: injectLayer

readonly c = injectLayer(confirm);
await this.c.open({ title: "Remove?" });

Subscribing to stacks

Options-bag + optional trailing client; select (not selector).

useStack / injectQueuedStack

readonly stack = useStack({ stack: "confirm" });
readonly count = useStack({ stack: "confirm", select: (s) => s.length });
readonly queued = injectQueuedStack({ stack: "confirm" });

injectLayerState / injectLayerQueuedState

Observe-only; Signal<LayerState[]> for all same-key instances:

readonly states = injectLayerState({ key: confirm.key, stack: "confirm" });
readonly top = computed(() => this.states().at(-1));

Rendering layers

renderStack (recommended)

Call renderStack(vcr, stack?, rootProps?) in an injection context. Each active layer's registered component is created into vcr; missing component renders nothing and warns in development.

Layer components declare @Input() fields matching LayerComponentProps — at minimum call and payload; renderStack also sets data, error, phase, transition, actionStatus, and dismissing.

import { Component, Input, ViewContainerRef } from "@angular/core";
import {
  renderStack,
  type LayerComponentProps,
} from "@stainless-code/angular-layers";

@Component({
  selector: "app-confirm-dialog",
  standalone: true,
  template: `<div role="dialog">…</div>`,
})
export class ConfirmDialogComponent {
  @Input({ required: true })
  call!: LayerComponentProps<ConfirmPayload, ConfirmResponse>["call"];
  @Input({ required: true }) payload!: ConfirmPayload;
  @Input({ required: true }) actionStatus!: LayerComponentProps<
    ConfirmPayload,
    ConfirmResponse
  >["actionStatus"];
}

@Component({
  selector: "app-stack-host",
  standalone: true,
  template: `<ng-container #outlet />`,
})
export class StackHostComponent {
  constructor(vcr: ViewContainerRef) {
    renderStack(vcr, "confirm");
  }
}

useStackHandles (headless)

For fully custom hosts, useStackHandles(stack?, rootProps?) returns { states: Signal<LayerState[]>, getCall }. Iterate states() in @for and pass getCall(state) to your own components:

import { Component } from "@angular/core";
import {
  useStackHandles,
  type LayerCallContext,
  type LayerState,
} from "@stainless-code/angular-layers";

@Component({
  selector: "app-confirm-outlet",
  standalone: true,
  template: `
    @for (state of handles.states(); track state.id) {
      <app-confirm-dialog
        [call]="handles.getCall(state)"
        [payload]="state.payload"
      />
    }
  `,
})
export class ConfirmOutletComponent {
  readonly handles = useStackHandles("confirm");
}

Rendering surface

The package is compiler-free — no shipped Angular components. Call renderStack(vcr, stack) or group.renderInto(vcr) / stackHook.renderInto(vcr) for outlets; use useStack({ select }) when you need a projected signal instead of a host.

Nested layers

Use useLayerGroup(call, options?) inside a layer component (injection context). Parent dismiss / unmount cancelAlls the child stack (LayerCancelledError); dispose() unbinds the lifetime hook.

import {
  Component,
  inject,
  Injector,
  Input,
  runInInjectionContext,
  ViewChild,
  ViewContainerRef,
} from "@angular/core";
import {
  useLayerGroup,
  type LayerCallContext,
  type LayerGroup,
} from "@stainless-code/angular-layers";

@Component({
  selector: "app-parent-drawer",
  standalone: true,
  template: `
    <button type="button" (click)="openChild()">Advanced</button>
    <ng-container #childOutlet />
  `,
})
export class ParentDrawerComponent {
  @Input({ required: true }) call!: LayerCallContext<unknown, unknown>;

  @ViewChild("childOutlet", { read: ViewContainerRef, static: true })
  childOutlet!: ViewContainerRef;

  private readonly injector = inject(Injector);
  private group!: LayerGroup;

  ngOnInit() {
    runInInjectionContext(this.injector, () => {
      this.group = useLayerGroup(this.call, { name: "advanced" });
      this.group.renderInto(this.childOutlet);
    });
  }

  openChild() {
    void this.group.open({ key: ["drawer", "child"], payload: { step: 2 } });
  }
}

Returns { open, dismissAll, states, stackId, renderInto }. For low-level control, core createLayerGroup(client, parent, options?) is still available via re-export.

Async actions

useMutationFlow(call) coordinates pending state with async work and ends the layer on success:

import { Component, Input } from "@angular/core";
import {
  useMutationFlow,
  type LayerComponentProps,
} from "@stainless-code/angular-layers";

@Component({
  selector: "app-save-dialog",
  standalone: true,
  template: `
    <button [disabled]="flow.pending()" (click)="save()">Confirm</button>
  `,
})
export class SaveDialogComponent {
  @Input({ required: true })
  call!: LayerComponentProps<ConfirmPayload, ConfirmResponse>["call"];

  readonly flow = useMutationFlow(this.call);

  save() {
    void this.flow.run(() => this.persist()).orEnd(true);
  }

  private async persist() {
    await saveToServer();
  }
}

pending is a Signal<boolean> — call flow.pending() in templates. run(fn).orEnd(response) sets actionStatus: "running", runs fn, then calls call.end(response); failures leave the layer open and rethrow.

For manual control, call.setRunning(true) / call.setRunning(false) in try/finally still works.

App chrome factory

Bind a stack once with createStackHook({ stack?, client? }):

import { Component, ViewContainerRef } from "@angular/core";
import { createStackHook } from "@stainless-code/angular-layers";

const confirmStack = createStackHook({ stack: "confirm" });

// app.config.ts providers: [confirmStack.provideClient()]
// or confirmStack.provideClient(existingClient)

@Component({
  selector: "app-confirm-root",
  standalone: true,
  template: "",
})
export class ConfirmRootComponent {
  constructor(vcr: ViewContainerRef) {
    confirmStack.renderInto(vcr);
  }
}

@Component({
  selector: "app-opener",
  standalone: true,
  template: `<button type="button" (click)="remove()">Remove</button>`,
})
export class OpenerComponent {
  private readonly stack = confirmStack.useAppStack();

  async remove() {
    await this.stack.open({ ...confirm, payload: { title: "Remove?" } });
  }
}

Returns { provideClient, useAppStack, renderInto } — register provideClient() in app config, then call renderInto(vcr) from a host component to mount the bound stack (see § Rendering surface).

Adapter API

Name Signature / shape
LAYER_CLIENT InjectionToken<LayerClient>
provideLayerClient (client?: LayerClient) => FactoryProvider
useLayerClient () => LayerClient
injectLayer (options, client?) => WiredLayerHandle signals + state/queued/top
injectLayerState ({ key, stack?, select?, compare? }, client?) => Signal<LayerState[]>
injectLayerQueuedState ({ key, stack?, select?, compare? }, client?) => Signal<LayerState[]>
useStack ({ stack?, select?, compare? }, client?) => Signal<T>
injectQueuedStack ({ stack?, select?, compare? }, client?) => Signal<T>
renderStack (vcr, stack?, rootProps?) => void — imperative outlet; injection context
useStackHandles (stack?, rootProps?) => { states, getCall }
useMutationFlow (call) => { pending: Signal<boolean>, run }
useLayerGroup (call, options?) => { open, dismissAll, states, stackId, renderInto }
createStackHook ({ stack?, client? }) => { provideClient, useAppStack, renderInto }

Types: StackHandles, MutationRun<R>, MutationFlow<R>, ScopedOpen, LayerGroup, AppStack, StackHook.

Core re-exports (same import path)

Core exports are available from @stainless-code/angular-layers, including LayerClient, LayerStack, layerOptions, layerKey, LayerState, LayerCallContext, LayerComponentProps, createCallContext, createLayerGroup, DataTag, ResponseOf, and ErrorOf:

  • Key inference: layerOptions / layerKey DataTag branding — await client.open(...) infers R.
  • Singleton + live updates: upsert: true on open; client.getStack(id).update(layer, patch).
  • Serial scope: new LayerClient({ defaultStackOptions: { confirm: { scope: { strategy: "serial" } } } }) — pass to provideLayerClient(client).
  • Validation: validate on layerOptions or open; narrow PayloadValidationError via isPayloadValidationError.
  • Blockers: call.addBlocker / stack.addBlocker; dismissing flag; dismissAll modes.

See the layers core skill for full engine coverage.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/stainless-code-layers-angular-layers/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

stainless-code-layers-angular-layers.ocm.jsonjson
{
  "ocm": "1",
  "id": "stainless-code-layers-angular-layers",
  "kind": "skill",
  "name": "angular-layers",
  "description": "Angular adapter for @stainless-code/angular-layers (signals + DI + imperative renderStack); use when wiring provideLayerClient, renderStack/useStackHandles, useLayerGroup, useMutationFlow, or createStackHook",
  "publisher": "stainless-code",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "tanstack-intent",
      "angular",
      "modal",
      "dialog",
      "stack",
      "typescript",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Angular adapter for @stainless-code/angular-layers (signals + DI + imperative renderStack); use when wiring provideLayerClient, renderStack/useStackHandles, useLayerGroup, useMutationFlow, or createStackHook"
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/stainless-code/layers",
      "path": "packages/angular/skills/angular-layers/SKILL.md",
      "ref": "31b44fcbea2b0cc55b25329e26a09cd9871d3795",
      "url": "https://github.com/stainless-code/layers/blob/31b44fcbea2b0cc55b25329e26a09cd9871d3795/packages/angular/skills/angular-layers/SKILL.md",
      "key": "stainless-code/layers/packages/angular/skills/angular-layers/SKILL.md"
    },
    "license": "MIT"
  },
  "instructions": "# Angular layer/stack UI with @stainless-code/angular-layers\n\nOpen any layer from anywhere in Angular and manage modal, dialog, drawer, popover, and toast UI as ordered, named stacks. Awaiting a typed result and fire-and-forget invocation (`void client.open(...)`) are equally first-class; named stacks, singletons via `upsert` with live `update`, serial queues, nested child stacks, transitions, dismissal blockers, and validation also provide standalone value.\n\nThe package re-exports `@stainless-code/layers`, so adapter and core APIs share one import path. It builds compiler-free with tsdown — e",
  "cost": {
    "context_tokens": 3993
  }
}

Fetch it by URL: GET /api/v1/registry/stainless-code-layers-angular-layers/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.