Alert Dialog

A modal that asks for an explicit choice.

import { โ€ฆ } from '@inlaylab/ivorie/alert-dialog'

Last close: none

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IvButton } from '@inlaylab/ivorie/button';
import { AlertDialogCloseEvent, IV_ALERT_DIALOG_IMPORTS } from '@inlaylab/ivorie/alert-dialog';

@Component({
  selector: 'demo-alert-dialog',
  imports: [IV_ALERT_DIALOG_IMPORTS, IvButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-2">
      <button ivButton variant="outline" [ivAlertDialogTrigger]="confirmTpl" (closed)="last.set($event)">
        Delete account
      </button>
      <p class="text-sm text-muted-foreground">Last close: {{ last()?.reason ?? 'none' }}</p>
    </div>

    <ng-template #confirmTpl>
      <iv-alert-dialog-header>
        <iv-alert-dialog-title>Delete account?</iv-alert-dialog-title>
        <iv-alert-dialog-description>
          This action cannot be undone. All your data will be permanently removed.
        </iv-alert-dialog-description>
      </iv-alert-dialog-header>
      <iv-alert-dialog-footer>
        <button ivAlertDialogCancel>Cancel</button>
        <button ivAlertDialogAction variant="destructive">Delete</button>
      </iv-alert-dialog-footer>
    </ng-template>
  `,
})
export default class AlertDialogDemo {
  protected readonly last = signal<AlertDialogCloseEvent | null>(null);
}

Installation

CLInpm
npx ivorie add alert-dialog

A confirm dialog on CDK Overlay. A backdrop click never closes it. The user picks Cancel or Action.

Usage

import { IvAlertDialog, IV_ALERT_DIALOG_IMPORTS } from '@inlaylab/ivorie/alert-dialog';
<button ivButton [ivAlertDialogTrigger]="confirmTpl" (closed)="onClosed($event)">
  Delete account
</button>

<ng-template #confirmTpl>
  <iv-alert-dialog-header>
    <iv-alert-dialog-title>Delete account?</iv-alert-dialog-title>
    <iv-alert-dialog-description>This action cannot be undone.</iv-alert-dialog-description>
  </iv-alert-dialog-header>
  <iv-alert-dialog-footer>
    <button ivAlertDialogCancel>Cancel</button>
    <button ivAlertDialogAction variant="destructive">Delete</button>
  </iv-alert-dialog-footer>
</ng-template>
private alertDialog = inject(IvAlertDialog);
readonly confirmTpl = viewChild.required<TemplateRef<unknown>>('confirmTpl');

deleteUser() {
  const ref = this.alertDialog.open(this.confirmTpl(), { size: 'sm' });
  ref.afterClosed().subscribe(({ reason }) => {
    if (reason === 'action') this.doDelete();
  });
}

Examples

Service

import { ChangeDetectionStrategy, Component, TemplateRef, inject, signal, viewChild } from '@angular/core';
import { IvButton } from '@inlaylab/ivorie/button';
import { IV_ALERT_DIALOG_IMPORTS, IvAlertDialog } from '@inlaylab/ivorie/alert-dialog';

@Component({
  selector: 'demo-alert-dialog-service',
  imports: [IV_ALERT_DIALOG_IMPORTS, IvButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-2">
      <button ivButton variant="outline" (click)="remove()">Remove user</button>
      @if (removed()) {
        <p class="text-sm text-muted-foreground">User removed.</p>
      }
    </div>

    <ng-template #confirmTpl>
      <iv-alert-dialog-header>
        <iv-alert-dialog-title>Remove user?</iv-alert-dialog-title>
        <iv-alert-dialog-description>They lose access right away.</iv-alert-dialog-description>
      </iv-alert-dialog-header>
      <iv-alert-dialog-footer>
        <button ivAlertDialogCancel>Cancel</button>
        <button ivAlertDialogAction variant="destructive">Remove</button>
      </iv-alert-dialog-footer>
    </ng-template>
  `,
})
export default class AlertDialogServiceDemo {
  private readonly alertDialog = inject(IvAlertDialog);
  private readonly confirmTpl = viewChild.required<TemplateRef<unknown>>('confirmTpl');
  protected readonly removed = signal(false);

  remove() {
    this.alertDialog
      .open(this.confirmTpl())
      .afterClosed()
      .subscribe(({ reason }) => this.removed.set(reason === 'action'));
  }
}

Media icon

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideTrash2 } from '@lucide/angular';
import { IvButton } from '@inlaylab/ivorie/button';
import { IV_ALERT_DIALOG_IMPORTS } from '@inlaylab/ivorie/alert-dialog';

@Component({
  selector: 'demo-alert-dialog-media',
  imports: [IV_ALERT_DIALOG_IMPORTS, IvButton, LucideTrash2],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button ivButton variant="outline" [ivAlertDialogTrigger]="tpl">Delete account</button>

    <ng-template #tpl>
      <iv-alert-dialog-header>
        <iv-alert-dialog-media>
          <svg lucideTrash2></svg>
        </iv-alert-dialog-media>
        <iv-alert-dialog-title>Delete account?</iv-alert-dialog-title>
        <iv-alert-dialog-description>This cannot be undone.</iv-alert-dialog-description>
      </iv-alert-dialog-header>
      <iv-alert-dialog-footer>
        <button ivAlertDialogCancel>Cancel</button>
        <button ivAlertDialogAction variant="destructive">Delete</button>
      </iv-alert-dialog-footer>
    </ng-template>
  `,
})
export default class AlertDialogMediaDemo {}

Result from action

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IvButton } from '@inlaylab/ivorie/button';
import { AlertDialogCloseEvent, IV_ALERT_DIALOG_IMPORTS } from '@inlaylab/ivorie/alert-dialog';

@Component({
  selector: 'demo-alert-dialog-result',
  imports: [IV_ALERT_DIALOG_IMPORTS, IvButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-2">
      <button ivButton variant="outline" [ivAlertDialogTrigger]="tpl" (closed)="onClosed($event)">
        Archive project
      </button>
      @if (archivedId()) {
        <p class="text-sm text-muted-foreground">Archived project {{ archivedId() }}</p>
      }
    </div>

    <ng-template #tpl>
      <iv-alert-dialog-header>
        <iv-alert-dialog-title>Archive project 42?</iv-alert-dialog-title>
        <iv-alert-dialog-description>You can restore it later.</iv-alert-dialog-description>
      </iv-alert-dialog-header>
      <iv-alert-dialog-footer>
        <button ivAlertDialogCancel>Cancel</button>
        <button [ivAlertDialogAction]="42">Archive</button>
      </iv-alert-dialog-footer>
    </ng-template>
  `,
})
export default class AlertDialogResultDemo {
  protected readonly archivedId = signal<number | null>(null);

  onClosed({ reason, result }: AlertDialogCloseEvent) {
    if (reason === 'action') this.archivedId.set(result as number);
  }
}

Small size

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IvButton } from '@inlaylab/ivorie/button';
import { IV_ALERT_DIALOG_IMPORTS } from '@inlaylab/ivorie/alert-dialog';

@Component({
  selector: 'demo-alert-dialog-small',
  imports: [IV_ALERT_DIALOG_IMPORTS, IvButton],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button ivButton variant="outline" [ivAlertDialogTrigger]="tpl" [ivAlertDialogConfig]="{ size: 'sm' }">
      Sign out
    </button>

    <ng-template #tpl>
      <iv-alert-dialog-header>
        <iv-alert-dialog-title>Sign out?</iv-alert-dialog-title>
        <iv-alert-dialog-description>You will need to sign in again.</iv-alert-dialog-description>
      </iv-alert-dialog-header>
      <iv-alert-dialog-footer>
        <button ivAlertDialogCancel>Stay</button>
        <button ivAlertDialogAction>Sign out</button>
      </iv-alert-dialog-footer>
    </ng-template>
  `,
})
export default class AlertDialogSmallDemo {}

API

Every part also accepts class.

IvAlertDialog

Service, providedIn: 'root'.

Method Signature Description
open <T, R, D>(content: Type<T> | TemplateRef<unknown>, config?: AlertDialogConfig<D>) => IvAlertDialogRef<T, R> Opens an alert dialog.

A template gets $implicit (the ref), data and dialogRef in its context. A component gets IvAlertDialogRef and IV_ALERT_DIALOG_DATA by injection.

IvAlertDialogRef

Member Signature Description
close (result?: R, reason?: AlertDialogCloseReason) => void Closes the dialog. reason defaults to 'programmatic'. Only the first close counts.
afterClosed () => Observable<AlertDialogCloseEvent<R>> Emits once, then completes.
closed Signal<AlertDialogCloseEvent<R> | undefined> undefined while open, then the close event.
componentInstance T | undefined The live instance when opened with a component.

AlertDialogConfig

Name Type Default Description
data D null Injected as IV_ALERT_DIALOG_DATA. Also in the template context as data.
size 'default' | 'sm' 'default' Width and footer layout. sm makes the footer a two-column grid.
disableEscapeClose boolean false Escape does not close.
panelClass string | string[] none Extra classes on the overlay panel.

AlertDialogCloseEvent

{ reason: AlertDialogCloseReason; result?: R }.

Reason Meaning
'action' An ivAlertDialogAction button.
'cancel' An ivAlertDialogCancel button.
'escape' Escape key.
'programmatic' A close() call with no reason.
'navigation' The overlay was disposed from outside, for example by a route change.

IV_ALERT_DIALOG_DATA

Injection token for the data passed to open.

ivAlertDialogTrigger

Opens an alert dialog on click.

Name Type Default Description
ivAlertDialogTrigger Type<T> | TemplateRef<unknown> required What to open (class field content).
ivAlertDialogConfig AlertDialogConfig<D> {} Config for open.

Outputs:

Name Payload Description
opened IvAlertDialogRef<T, R> Fires when the dialog opens.
closed AlertDialogCloseEvent<R> Fires when the dialog closes.

button[ivAlertDialogAction]

Closes with reason 'action'. Styled as a button, so do not add ivButton.

Name Type Default Description
ivAlertDialogAction R undefined Result passed to close (class field result).
variant ButtonVariants['variant'] 'default' Button variant.
size ButtonVariants['size'] 'default' Button size.

button[ivAlertDialogCancel]

Closes with reason 'cancel'. Styled as a button, so do not add ivButton.

Name Type Default Description
ivAlertDialogCancel R undefined Result passed to close (class field result).
variant ButtonVariants['variant'] 'outline' Button variant.
size ButtonVariants['size'] 'default' Button size.

Layout parts. No inputs. Put iv-alert-dialog-media first in the header: the grid relies on source order.

Accessibility

  • The panel has role="alertdialog" and aria-modal="true".
  • It is labelled by iv-alert-dialog-title and described by iv-alert-dialog-description.
  • Focus is trapped inside the panel. Cancel gets initial focus: the least destructive choice.
  • On close, focus returns to the element that was focused before open. It is not moved if focus is already somewhere else outside the dialog. This runs before afterClosed emits.
  • Escape closes the dialog, unless disableEscapeClose is set. Only the topmost overlay closes.
  • Body scroll is blocked while open.

Notes

  • Action and Cancel inject IvAlertDialogRef, so use them inside a template or component opened by the service.
  • size is set in the open config only. Layout children follow it.
  • For a strict flow, set disableEscapeClose: true.