ComponentCDK DialogUpdated in v15

Dialog

Open any component as a modal dialog and await its result.

Installation

Import the class, and @use the entry point in your global styles: without it the component renders unstyled.

import { Component } from '@angular/core';
import { WR_DIALOG_DATA, WrDialog, WrDialogClose, WrDialogContent, WrDialogFooter, WrDialogTitle } from 'ngwr/dialog';

@Component({ imports: [WrDialogClose, WrDialogContent, WrDialogFooter, WrDialogTitle] })
export class MyComponent {}

Open a dialog

open() returns a ref; awaitClose() resolves with the value the dialog closed with.

const ref = dialog.open(ConfirmComponent, {
  data: { title: 'Delete', message: 'Are you sure?' },
  width: '24rem',
});

const ok = await ref.awaitClose();
<!-- Inside the opened component.
     imports: [WrDialogTitle, WrDialogContent, WrDialogFooter, WrDialogClose] -->
<h2 wrDialogTitle>Delete</h2>
<div wrDialogContent>Are you sure?</div>
<div wrDialogFooter>
  <wr-btn wrDialogClose>Cancel</wr-btn>
  <wr-btn color="danger" [wrDialogClose]="true">Delete</wr-btn>
</div>
Open confirm dialog Last result: —
dialog.open(ConfirmComponent, { data, width: '24rem' })

Closing from the content

Inject WrDialogRef and call close(result); [wrDialogClose] does the same on click.

import { WR_DIALOG_DATA, WrDialogRef } from 'ngwr/dialog';

@Component({...})
export class EditUserComponent {
  private readonly ref = inject<WrDialogRef<EditUserComponent, boolean>>(WrDialogRef);
  protected readonly data = inject<EditUserData>(WR_DIALOG_DATA);

  save(): void {
    this.store.saveUser(this.form.value);
    this.ref.close(true);
  }
}

Lifetime

A dialog outlives the component that opened it. It closes on navigation; anything else is yours to close.

@Component({...})
export class MyComponent {
  private readonly dialog = inject(WrDialog);
  private readonly destroyRef = inject(DestroyRef);

  async confirmDelete(): Promise<void> {
    const ref = this.dialog.open<ConfirmComponent, boolean>(ConfirmComponent);
    const stop = this.destroyRef.onDestroy(() => ref.close());

    const ok = await ref.awaitClose();
    stop();
    if (ok) this.store.remove();
  }
}

// Or subscribe:
ref.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(ok => { … });

The close button

A ✕ is shown by default. closable: false hides it, closeLabel renames it.

dialog.open(EditUserComponent, { closable: false });
dialog.open(EditUserComponent, { closeLabel: 'Discard changes' });

Focus

Focus moves to cdkFocusInitial, else to the first tabbable element, and returns to the trigger on close.

<!-- cdkFocusInitial needs no import. -->
<h2 wrDialogTitle>Rename project</h2>
<div wrDialogContent>
  <wr-form-field label="Name">
    <input wrInput cdkFocusInitial [(value)]="name" />
  </wr-form-field>
</div>
<div wrDialogFooter>
  <wr-btn wrDialogClose>Cancel</wr-btn>
  <wr-btn color="primary" [wrDialogClose]="name()">Save</wr-btn>
</div>

Gotchas

What happensWhat to do
A click outside an open select inside the dialog closes the select and the dialog.Open a dialog that holds a form with closeOnBackdropClick: false.
No z-index puts your element above a dialog: overlays live in the top layer. Make that element a popover too, or provide OVERLAY_DEFAULT_CONFIG with { usePopover: false }.
An ngwr overlay inside a native <dialog> opened with showModal() ignores clicks. Use WrDialog instead of the native dialog.
If the dialog's result removes its trigger, focus falls back to <body>. Move focus yourself after awaitClose().

Service API

NameDescriptionTypeDefault
open(component, options?)Opens a dialog. Returns a WrDialogRef.(component, WrDialogOptions) => WrDialogRef—

WrDialogRef

Every dismissal closes with undefined, so close with a value of your own to tell them apart.

NameDescriptionTypeDefault
WrDialogRef<C, R>Returned by open() and injectable inside the dialog. C is the component, R the result.class—
close(result?)Close the dialog, optionally with a result. A second call does nothing.(result?: R) => void—
awaitClose()Resolves with the close result.() => Promise<R | undefined>—
closedThe same result as an Observable. Emits once, then completes; a late subscriber still gets it.ReplaySubject<R | undefined>—
componentInstanceThe opened component. Throws if read from its own constructor.C—
overlayRefThe underlying CDK OverlayRef. Never dispose it; call close().OverlayRef—
const ref = this.dialog.open<ConfirmComponent, 'saved' | 'discarded'>(ConfirmComponent);

const result = await ref.awaitClose(); // 'saved' | 'discarded' | undefined
if (result === undefined) return;      // dismissed

WrDialogOptions

NameDescriptionTypeDefault
WrDialogOptionsSecond argument of open(). Every field is optional.interface—
dataPayload exposed to the content via WR_DIALOG_DATA.D—
widthPanel width — any CSS length.string—
maxWidthPanel maximum width.string—
closeOnBackdropClickClose when the backdrop is clicked.booleantrue
closeOnEscapeClose on Escape.booleantrue
closeOnNavigationClose when the URL changes.booleantrue
closableShow the built-in dismiss (×) in the top-right corner.booleantrue
closeLabelAccessible name for the dismiss button. Falls back to the dialog.close catalog key.string—
panelClassExtra class(es) on the panel.string | readonly string[]—

Layout directives

NameDescriptionTypeDefault
[wrDialogTitle]Styles the title row, and supplies the panel’s aria-labelledby. Import WrDialogTitle.directive—
[wrDialogContent]Styles the scrollable body. Import WrDialogContent.directive—
[wrDialogFooter]Styles the footer. Import WrDialogFooter.directive—
alignFooter alignment.'start' | 'center' | 'end''end'
[wrDialogClose]Closes the dialog when clicked. Import WrDialogClose.directive—
wrDialogCloseValue passed to close(). Bare attribute closes with undefined.R | undefinedundefined

Available inside the dialog

NameDescriptionTypeDefault
WR_DIALOG_DATAThe data payload passed to open(). undefined when you didn't pass any.InjectionToken<D>—
WrDialogRefThe open dialog’s own ref — call close(result) to dismiss it from the content.WrDialogRef<unknown, unknown>—
WR_DIALOG_REFThe same ref, used by [wrDialogClose]. Prefer inject(WrDialogRef).InjectionToken<WrDialogRef<C, R>>—

CSS variables

Set these on the component's selector or a wrapper, not on :root: the defaults are declared on ngwr/dialog's own element.

VariableDefaultDeclared on
--wr-dialog-bgvar(--wr-color-surface).wr-dialog-panel
--wr-dialog-bordervar(--wr-border-base).wr-dialog-panel
--wr-dialog-close-font-sizevar(--wr-text-base).wr-dialog-panel
--wr-dialog-close-paddingvar(--wr-space-xs).wr-dialog-panel
--wr-dialog-close-radiusvar(--wr-border-radius-sm).wr-dialog-panel
--wr-dialog-content-font-sizevar(--wr-text-base).wr-dialog-panel
--wr-dialog-content-line-heightvar(--wr-text-base-leading).wr-dialog-panel
--wr-dialog-font-familyvar(--wr-font-family-base).wr-dialog-panel
--wr-dialog-footer-gapvar(--wr-space-sm).wr-dialog-panel
--wr-dialog-min-width18rem.wr-dialog-panel
--wr-dialog-padding-xvar(--wr-space-lg).wr-dialog-panel
--wr-dialog-padding-yvar(--wr-space-lg).wr-dialog-panel
--wr-dialog-radiusvar(--wr-border-radius-lg).wr-dialog-panel
--wr-dialog-shadowvar(--wr-shadow-modal).wr-dialog-panel
--wr-dialog-title-font-sizevar(--wr-text-lg).wr-dialog-panel
--wr-dialog-title-font-weightvar(--wr-font-weight-semibold).wr-dialog-panel
--wr-dialog-title-line-heightvar(--wr-text-base-leading).wr-dialog-panel

Dependencies

Angular CDKa11y, overlay, portal, scrolling
Built onOverlay

See also