Open a shell-brokered modal
A modal is a surface used as a top-level overlay. The shell owns the frame — the backdrop, the escape key, the size clamp, the stacking above all chrome. You own the body — it is one of your app’s own surfaces, rendering arbitrary UI and doing its own backend work.
Open the modal
Section titled “Open the modal”Call platform.modal.open with the name of one of your own surfaces. It resolves with whatever the
body passes to close(), or null if the user dismisses it.
import { Component, inject } from "@angular/core";import { PLATFORM } from "../shared/platform.token";
interface Customer { id: string; name: string; email: string; creditStatus: string;}
// Opening a shell-brokered modal. The shell owns the frame, the backdrop, and the escape key; the// modal's BODY is one of this app's own surfaces (here "customer-edit"). modal.open resolves with// whatever the body passes to conn.close(), or null if the user dismisses it.@Component({ selector: "customer-row", template: `<button (click)="edit()">Edit</button>`,})export class CustomerRow { private readonly platform = inject(PLATFORM);
async edit(): Promise<void> { const updated = await this.platform.modal.open<Customer>({ name: "customer-edit", params: { id: "c_1", name: "Ada", email: "ada@example.com", creditStatus: "good" }, title: "Edit customer", width: 460, }); if (updated) { this.onSaved(updated); } }
private onSaved(_customer: Customer): void { // refresh the row with the saved record }}Pass presentation: "sheet" for a content-heavy body (like an edit form) that should fill the screen
on phones; the default "card" keeps the centered, viewport-capped panel. The option only changes
behaviour below the shell’s compact breakpoint — on desktop both render as the centered card.
Write the body
Section titled “Write the body”The body connects like any surface (SURFACE_CONN). The one thing it must get right is height
timing.
import { afterNextRender, ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, signal,} from "@angular/core";import { SURFACE_CONN } from "../shared/surface.token";
interface EditParams { id: string; name: string;}
// The modal's body is the app's OWN UI, connected like any surface (SURFACE_CONN). The height gotcha:// report height and signal ready only AFTER the body is laid out AND one extra frame has passed, so// the FIRST height the shell sees is the final height. The shell then reveals the modal already sized// — never clipped, no open-time grow. This is the one bug that types cannot catch.@Component({ selector: "customer-edit", changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form (submit)="save($event)"> <ds-input [value]="name()" (input)="name.set(value($event))"></ds-input> <ds-button variant="primary" type="submit">Save</ds-button> </form> `,})export class CustomerEdit { private readonly conn = inject(SURFACE_CONN); private readonly host = inject<ElementRef<HTMLElement>>(ElementRef); protected readonly name = signal("");
constructor() { // Seed the form from the opener's params before first paint. const p = (this.conn.params ?? {}) as Partial<EditParams>; this.name.set(p.name ?? "");
afterNextRender(() => { requestAnimationFrame(() => { this.conn.observe(this.host.nativeElement); this.conn.ready(); }); }); }
protected value(e: Event): string { return (e.target as HTMLInputElement).value; }
protected save(e: Event): void { e.preventDefault(); // Hand the result back to whoever opened the modal; close() resolves their modal.open promise. this.conn.close({ id: "c_1", name: this.name() }); }}See also
Section titled “See also”- Provide and host an embedded surface — the same surface mechanism, hosted inline.
ModalOpenOptionsandSurfaceConnection.closein the reference.