Skip to content

ALERTS MODULE

María Fátima García Luque edited this page Jun 25, 2026 · 1 revision

Alerts Module — @fireflyframework/core/alerts

Headless notification and confirmation system based on signals. Four presentation formats (toast, banner, bottom-sheet, dialog), six semantic types, centralized state management. Package: @fireflyframework/core · CORE Module #17 · Platform + Embedded


Table of Contents


1. Overview

The alerts module addresses the universal need to notify the user and request confirmations in any application.

What it does

  • Manages alert state as reactive signals (activeToasts(), activeBanners(), activeBottomSheets(), activeDialogs())
  • Supports four presentation formats:
    • Toast — compact, auto-dismissible notification in the upper right corner
    • Banner — full-width bar at the top of the viewport
    • Bottom Sheet — modal panel anchored at the bottom with actions
    • Dialog — centered modal with a result promise
  • Applies six semantic types: success, error, warning, info, destructive, custom
  • Manages auto-dismiss with configurable timers
  • Offers convenience shortcuts (success(), error(), warning(), info(), confirm())
  • Limits the number of alerts visible simultaneously (configurable)

What it does NOT do

  • Does not render UI — it is a headless state manager. The DS components (ff-toast, ff-banner, ff-bottom-sheet, ff-dialog) render the alerts
  • Does not decide layout — the product places AlertHostComponent wherever it sees fit (typically in the shell)
  • Does not persist — alerts live in memory. They are lost on page reload
  • Does not intercept HTTP errors — the product decides when and which alerts to fire
  • Does not know predefined messages — texts are defined by the product

Why it is a framework piece

  • Contains no business logic — it works with generic strings (messages, types)
  • It is headless — it separates state from presentation
  • It is reactive — all state is exposed as signals
  • It prevents each product from reimplementing the notification system inconsistently
  • Decoupled from the Design System — it works with any UI layer

Key architectural decision: HEADLESS

The alerts module deliberately follows a headless pattern:

AlertService (core)          →  State + logic + timers
AlertHostComponent (shell)   →  Reads signals, renders DS components
ff-toast/ff-banner/... (DS)  →  Pure presentational components

The service never creates or manipulates DOM. The host never manages state. The DS components never know about the service. This separation allows:

  • Replacing the DS components without touching the service
  • Testing the service without DOM
  • Reusing the service with a completely different DS

2. Developer Usage Guide

2.1 Installation and initial configuration

The module is activated by calling provideAlerts() in the product's app.config.ts:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAlerts } from '@fireflyframework/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAlerts(),
  ],
};

With custom configuration:

provideAlerts({
  defaultToastDuration: 5000,    // 5s instead of 3s
  maxVisibleToasts: 3,           // maximum 3 simultaneous toasts
  defaultToastPosition: 'bottom-left',
})

2.2 Toasts (ephemeral notifications)

Compact notifications that appear in a corner and auto-dismiss:

import { AlertService } from '@fireflyframework/core';

export class MyComponent {
  private readonly alerts = inject(AlertService);

  onSave(): void {
    this.alerts.success('Changes saved successfully.');
  }

  onError(): void {
    this.alerts.error('Something went wrong. Please try again.');
    // Errors last 5000ms by default (vs 3000ms for the rest)
  }

  onCustom(): void {
    this.alerts.toast('Custom notification.', 'info', {
      duration: 8000,          // 8 seconds
      dismissible: false,      // no close button
      icon: '📋',
    });
  }
}

2.3 Banners (persistent notifications)

Full-width bars for important notifications:

// Persistent banner with action
this.alerts.banner(
  'System maintenance tonight at 23:00 UTC.',
  'warning',
  {
    icon: '⚠',
    action: {
      label: 'Learn more',
      callback: () => this.router.navigate(['/maintenance']),
    },
  }
);

// Ephemeral banner (auto-dismiss in 5s)
this.alerts.banner('Your changes have been saved.', 'success', {
  duration: 5000,
  icon: '✓',
});

2.4 Bottom Sheets (modal panels)

Panels anchored at the bottom with actions:

this.alerts.bottomSheet('Choose an action for this item.', 'info', {
  title: 'Item Actions',
  actions: [
    { label: 'Edit', callback: () => this.edit(), type: 'info' },
    { label: 'Duplicate', callback: () => this.duplicate() },
    { label: 'Delete', callback: () => this.delete(), type: 'destructive' },
  ],
});

2.5 Dialogs (promise-based confirmations)

Modals that return a promise with the user's result:

// Simple confirmation
const confirmed = await this.alerts.confirm('Are you sure you want to proceed?');
if (confirmed) {
  this.doAction();
}

// Custom dialog
const result = await this.alerts.dialog({
  title: 'Export Data',
  message: 'Choose the export format.',
  type: 'info',
  confirmLabel: 'Export',
  cancelLabel: 'Cancel',
});

if (result.confirmed) {
  this.export();
}

2.6 Convenience shortcuts

The success(), error(), warning(), info() methods fire toasts by default but accept an alternative format:

this.alerts.success('Done!');                // toast success
this.alerts.success('Done!', 'banner');      // banner success
this.alerts.error('Failed.', 'dialog');      // dialog error

2.7 Clearing alerts

// Dismiss a specific toast
this.alerts.dismiss(toastId);

// Dismiss a specific banner
this.alerts.dismissBanner(bannerId);

// Dismiss a specific bottom sheet
this.alerts.dismissBottomSheet(sheetId);

// Clear everything (useful on logout or context change)
this.alerts.dismissAll();

2.8 Integrating the host into the shell

The product must place a host component that reads the service's signals and renders the DS components. Typical example:

// alert-host.component.ts
@Component({
  selector: 'app-alert-host',
  template: `
    <div class="alert-host__banners">
      @for (b of alerts.activeBanners(); track b.id) {
        <ff-banner [message]="b.message" [type]="b.type" ... />
      }
    </div>
    <div class="alert-host__toasts">
      @for (t of alerts.activeToasts(); track t.id) {
        <ff-toast [message]="t.message" [type]="t.type" ... />
      }
    </div>
    <!-- bottom sheets, dialogs... -->
  `,
})
export class AlertHostComponent {
  readonly alerts = inject(AlertService);
}
<!-- shell.component.html -->
<app-alert-host />
<app-sidebar />
<main><router-outlet /></main>

3. API Reference

3.1 Types

AlertType

type AlertType = 'success' | 'error' | 'warning' | 'info' | 'destructive' | 'custom';

Semantic type shared by the four formats.

AlertFormat

type AlertFormat = 'toast' | 'banner' | 'bottom-sheet' | 'dialog';

Presentation format for the convenience shortcuts.

Toast

interface Toast {
  readonly id: string;
  readonly message: string;
  readonly type: AlertType;
  readonly options: ToastOptions;
  readonly createdAt: number;
}

ToastOptions

interface ToastOptions {
  duration?: number;          // ms, default 3000 (error: 5000)
  position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
  dismissible?: boolean;      // default true
  icon?: string;
  customClass?: string;
  component?: unknown;        // custom Angular component
  componentData?: Record<string, unknown>;
}

Banner

interface Banner {
  readonly id: string;
  readonly message: string;
  readonly type: AlertType;
  readonly options: BannerOptions;
  readonly createdAt: number;
}

BannerOptions

interface BannerOptions {
  position?: 'top' | 'bottom';
  duration?: number;          // if omitted, persists until dismissed
  dismissible?: boolean;
  icon?: string;
  action?: { label: string; callback: () => void };
  customClass?: string;
  component?: unknown;
  componentData?: Record<string, unknown>;
}

BottomSheet

interface BottomSheet {
  readonly id: string;
  readonly message: string;
  readonly type: AlertType;
  readonly options: BottomSheetOptions;
  readonly createdAt: number;
}

BottomSheetOptions

interface BottomSheetOptions {
  title?: string;
  dismissible?: boolean;      // default true
  swipeToDismiss?: boolean;   // default true
  actions?: { label: string; callback: () => void; type?: AlertType }[];
  icon?: string;
  customClass?: string;
  component?: unknown;
  componentData?: Record<string, unknown>;
}

Dialog

interface Dialog {
  readonly id: string;
  readonly options: DialogOptions;
  readonly createdAt: number;
}

DialogOptions

interface DialogOptions {
  title?: string;
  message?: string;
  type: AlertType;                      // required
  confirmLabel?: string;                // default "Confirm"
  cancelLabel?: string;                 // if omitted, no Cancel button
  destructiveConfirmText?: string;      // confirmation text for destructive actions
  icon?: string;
  customClass?: string;
  component?: unknown;
  componentData?: Record<string, unknown>;
}

DialogResult

interface DialogResult {
  confirmed: boolean;
  input?: string;                       // user input if captured
}

AlertConfig

interface AlertConfig {
  defaultToastDuration?: number;        // default 3000
  defaultToastPosition?: ToastOptions['position'];
  defaultBannerPosition?: BannerOptions['position'];
  maxVisibleToasts?: number;            // default 5
  maxVisibleBanners?: number;           // default 3
}

3.2 AlertService

Registered via provideAlerts() — global singleton.

Signals (read-only)

Member Type Description
activeToasts() Signal<Toast[]> Active toasts. Read-only.
activeBanners() Signal<Banner[]> Active banners. Read-only.
activeBottomSheets() Signal<BottomSheet[]> Active bottom sheets. Read-only.
activeDialogs() Signal<Dialog[]> Active dialogs. Read-only.

Main methods

Method Returns Description
toast(message, type, options?) void Fires a toast. Auto-dismiss with timer.
banner(message, type, options?) void Fires a banner. Persists unless duration is set.
bottomSheet(message, type, options?) void Fires a modal bottom sheet.
dialog(options) Promise<DialogResult> Opens a modal dialog. Returns a promise with the result.
resolveDialog(id, result) void Resolves a pending dialog (called from the UI).

Convenience shortcuts

Method Returns Description
success(message, format?) void Success toast (or alternative format).
error(message, format?) void Error toast with 5000ms (or alternative format).
warning(message, format?) void Warning toast (or alternative format).
info(message, format?) void Info toast (or alternative format).
confirm(message, options?) Promise<boolean> Yes/no confirmation dialog.

Cleanup

Method Returns Description
dismiss(id) void Dismisses a toast by ID.
dismissBanner(id) void Dismisses a banner by ID.
dismissBottomSheet(id) void Dismisses a bottom sheet by ID.
dismissAll() void Clears all alerts, timers, and resolves pending dialogs as cancelled.

3.3 provideAlerts()

function provideAlerts(config?: AlertConfig): EnvironmentProviders

Module entry provider. Registers the configuration via the ALERT_CONFIG token. If config is omitted, defaults are applied.

3.4 ALERT_CONFIG

const ALERT_CONFIG: InjectionToken<AlertConfig>

Injection token for the global alert service configuration.

3.5 DS Components

The Design System components are purely presentational. They do not know about the AlertService. They are rendered by the host.

FfToastComponent

Input Type Default Description
message string '' Toast text
type FfToastVariant 'info' Semantic variant
icon string '' Optional icon
dismissible boolean true Shows close button
Output Type Description
dismissed void Emitted on close click

FfBannerComponent

Input Type Default Description
message string '' Banner text
type FfBannerVariant 'info' Semantic variant
icon string '' Optional icon
actionLabel string '' Action button text
dismissible boolean true Shows close button
Output Type Description
dismissed void Emitted on close click
actionClicked void Emitted on action click

FfBottomSheetComponent

Input Type Default Description
open boolean false Controls visibility
title string '' Panel title
message string '' Body message
type FfBottomSheetVariant 'info' Semantic variant
dismissible boolean true Close via backdrop/Escape/X
Output Type Description
dismissed void Emitted on close

Content projection: [ff-bottom-sheet-actions] for actions.

FfDialogComponent

Input Type Default Description
open boolean false Controls visibility
title string '' Dialog title
type FfDialogVariant undefined Optional semantic variant
dismissible boolean true Close via backdrop/Escape/X
Output Type Description
closed void Emitted on close

Content projection: [ff-dialog-actions] for actions.


4. Internal Technical Architecture

4.1 Design principles

  1. Headless — The service manages state, never DOM. The DS components render, never manage state. The separation is strict.
  2. Native signals — All state is exposed as Angular signals. No RxJS. No BehaviorSubjects.
  3. Four formats, one service — A single AlertService manages all four formats. Avoids fragmentation into ToastService, BannerService, etc.
  4. Promise-based dialogsdialog() returns a promise that resolves when the user interacts. The service stores internal resolvers.
  5. Limited capacity — Max 5 toasts and 3 banners simultaneously (configurable). Prevents visual overflow.
  6. Fire-and-forget toasts — Toasts auto-dismiss. The caller does not need to manage their lifecycle.

4.2 File structure

packages/core/src/lib/alerts/
├── index.ts                     # Public API barrel
├── alert.types.ts               # AlertType, AlertFormat, Toast, Banner, BottomSheet, Dialog, options, config
├── alert.service.ts             # AlertService (state + logic + timers)
├── alert.service.spec.ts        # Unit tests
└── provide-alerts.ts            # provideAlerts(), ALERT_CONFIG token
packages/design-system/src/lib/primitives/
├── ff-toast/                    # FfToastComponent (pure presentational)
├── ff-banner/                   # FfBannerComponent (pure presentational)
├── ff-bottom-sheet/             # FfBottomSheetComponent (pure presentational)
└── ff-dialog/                   # FfDialogComponent (pure presentational)

4.3 Data flow

┌──────────────────────────────────────────────────────────────────┐
│  Feature Component                                                │
│  alerts.success('Saved!')                                         │
│       │                                                           │
│       ▼                                                           │
│  AlertService                                                     │
│  _toasts.update(arr => [...arr, newToast])                        │
│  setTimeout(() => dismiss(id), duration)                          │
│       │                                                           │
│       ▼                                                           │
│  activeToasts() ──► Signal<Toast[]>                               │
│       │                                                           │
│       ▼                                                           │
│  AlertHostComponent (in shell)                                    │
│  @for (t of alerts.activeToasts(); track t.id) {                  │
│    <ff-toast [message]="t.message" [type]="t.type"                │
│              (dismissed)="alerts.dismiss(t.id)" />                │
│  }                                                                │
│       │                                                           │
│       ▼                                                           │
│  FfToastComponent (DS)                                            │
│  Renders message + icon + dismiss button                          │
│  Emits (dismissed) on user action                                 │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  Dialog flow (Promise-based)                                      │
│                                                                   │
│  const result = await alerts.dialog({ title, message, type })     │
│       │                                                           │
│       ▼                                                           │
│  AlertService                                                     │
│  _dialogs.update(arr => [...arr, newDialog])                      │
│  _pendingResolvers.set(id, { resolve })                           │
│  return new Promise(resolve => ...)                               │
│       │                                                           │
│       ▼                                                           │
│  AlertHostComponent                                               │
│  @for (d of alerts.activeDialogs(); track d.id) {                 │
│    <ff-dialog [open]="true" [title]="d.options.title" ...>        │
│      <ff-button (clicked)="alerts.resolveDialog(d.id, result)">  │
│    </ff-dialog>                                                   │
│  }                                                                │
│       │                                                           │
│       ▼                                                           │
│  resolveDialog(id, { confirmed: true })                           │
│  → Resolves the original promise                                  │
│  → Removes the dialog from the signal                             │
│  → The caller receives the result                                 │
└──────────────────────────────────────────────────────────────────┘

4.4 Internal dependencies

alert.service.ts
  ├── alert.types.ts             (local)
  └── ALERT_CONFIG               (local, optional inject)

provide-alerts.ts
  └── alert.types.ts             (local)

The module does not depend on other framework modules. It only depends on @angular/core. The DS components also do not depend on the service — they only receive inputs and emit outputs.

4.5 Architectural decisions

Decision Rationale
Headless (service without UI) Completely decouples state from presentation. Allows replacing the DS without touching the core. Allows testing without DOM. Follows the "headless UI" pattern popularized by Headless UI and Radix.
One service, four formats A single entry point avoids fragmentation. The caller chooses the format via method (toast(), banner(), bottomSheet(), dialog()). Internally, each format has its own signal.
Signals instead of BehaviorSubject Angular 21 signals are the native reactive primitive. They eliminate RxJS boilerplate. The host reads the signals directly in the template with @for.
Promise instead of Observable for dialogs Dialogs produce a single result. A promise is semantically correct and simpler than an Observable that emits once. async/await makes the caller's code more readable.
Auto-dismiss with setTimeout Toasts are ephemeral by nature. setTimeout is sufficient — they do not need observables or schedulers. Timers are cleared in dismissAll().
Configurable limited capacity Prevents visual overflow (5 stacked toasts, 3 banners). The limits are configured via provideAlerts(). When the limit is reached, the oldest toast is dismissed.
AlertHostComponent in the product, not in the framework The host is part of the product's shell, not the framework. The framework provides the service and types. The product decides where and how to render. This respects the public/private boundary.
DS components without state logic ff-toast, ff-banner, etc. are pure presentational. They receive inputs, emit events. They do not inject AlertService. They can be used standalone without the service (e.g. in a catalog playground).
@Injectable() + provideAlerts() AlertService is a global singleton — alerts are global to the application. Requires explicit registration via provideAlerts() in app.config.ts.
Errors with longer duration Toasts of type error last 5000ms instead of 3000ms. Errors are more important and require more reading time.

4.6 Relationship with other framework modules

Module Relationship
Design System No direct dependency. The DS components (ff-toast, ff-banner, ff-bottom-sheet, ff-dialog) are consumed by the product's host. The service does not import or know about the DS components.
NavigationService No dependency. Both modules are independent.
PermissionService No dependency. The product can combine them (e.g. "show an alert if the user lacks permission"), but the framework does not couple them.
Auth module (future) No dependency. The product can call dismissAll() on logout, but that is the product's decision.
Router (Angular) No dependency. Unlike the permissions module, the alerts service does not interact with the router.

5. Advanced Usage Patterns

Alert after an HTTP action

async onSave(): Promise<void> {
  try {
    await this.http.put('/api/items', this.form.value).toPromise();
    this.alerts.success('Item saved successfully.');
  } catch {
    this.alerts.error('Failed to save item. Please try again.');
  }
}

Destructive confirmation

async onDelete(item: Item): Promise<void> {
  const result = await this.alerts.dialog({
    title: 'Delete Item',
    message: `Are you sure you want to delete "${item.name}"? This cannot be undone.`,
    type: 'destructive',
    confirmLabel: 'Delete',
    cancelLabel: 'Keep',
  });

  if (result.confirmed) {
    await this.itemService.delete(item.id);
    this.alerts.success(`"${item.name}" deleted.`);
  }
}

Maintenance banner with action

this.alerts.banner(
  'Scheduled maintenance in 30 minutes. Save your work.',
  'warning',
  {
    icon: '🔧',
    action: {
      label: 'Details',
      callback: () => window.open('/status', '_blank'),
    },
  }
);

Cleanup on logout

onLogout(): void {
  this.alerts.dismissAll();
  this.permissionService.clear();
  this.router.navigate(['/login']);
}

Bottom sheet with multiple actions

this.alerts.bottomSheet('What would you like to do with this document?', 'info', {
  title: 'Document Actions',
  actions: [
    { label: 'Share', callback: () => this.share(), type: 'info' },
    { label: 'Download', callback: () => this.download() },
    { label: 'Archive', callback: () => this.archive(), type: 'warning' },
    { label: 'Delete', callback: () => this.onDelete(), type: 'destructive' },
  ],
});

Custom global configuration

// app.config.ts
provideAlerts({
  defaultToastDuration: 4000,
  maxVisibleToasts: 3,
  maxVisibleBanners: 1,
  defaultToastPosition: 'bottom-right',
  defaultBannerPosition: 'top',
})

Reactive state reading

// Show active-alerts indicator in the sidebar
readonly hasActiveAlerts = computed(() =>
  this.alerts.activeToasts().length +
  this.alerts.activeBanners().length +
  this.alerts.activeBottomSheets().length +
  this.alerts.activeDialogs().length > 0
);

Related Documents

  • Alerts Module

Clone this wiki locally