Source

lib/utils/NgxToast.ts

import { DecafToastOptions, DecafToastRole, IDecafToast } from '@decaf-ts/ui-decorators';
import { ToastOptions } from '@ionic/angular/standalone';
import { OverlayEventDetail, toastController } from '@ionic/core';

/**
 * @description Default toast configuration used by the toast utility.
 * @summary Provides the baseline Ionic toast settings applied before caller overrides are
 * merged. These defaults keep toast behavior consistent across the application.
 * @const {DecafToastOptions} DefaulOptions
 */
const DefaulOptions = {
  duration: 3000,
  position: 'top',
  animated: true,
};

/**
 * @description Cached singleton toast helper instance.
 * @summary Stores the shared `NgxToast` object returned by the factory so every caller uses
 * the same overlay state and toast lifecycle.
 *
 * @private
 * @type {NgxToast}
 */
let instance!: NgxToast;

/**
 * @description Active Ionic toast element reference.
 * @summary Tracks the toast element currently visible on screen so it can be dismissed,
 * replaced, or observed for dismissal events.
 *
 * @private
 * @type {HTMLIonToastElement | null}
 */
let component: HTMLIonToastElement | null;

/**
 * @description Toast notification helper for Ionic applications.
 * @summary Wraps Ionic's toast controller with a singleton-friendly API for showing
 * informational, success, warning, and error messages. The class centralizes overlay
 * creation, dismissal handling, and role extraction so application code stays small.
 * @class NgxToast
 * @implements {IDecafToast}
 *
 * @example
 * ```typescript
 * // Basic usage
 * const toast = getNgxToastComponent();
 * await toast.success('Operation completed successfully!');
 * await toast.error('Something went wrong!');
 *
 * // With custom options
 * const toast = getNgxToastComponent({
 *   duration: 5000,
 *   position: 'bottom'
 * });
 * await toast.inform('Custom toast message');
 *
 * // Handling dismissal role
 * const role = await toast.warn('Are you sure?');
 * if (role === 'cancel') {
 *   console.log('User cancelled');
 * }
 * ```
 *
 * @features
 * - Multiple toast types with semantic colors (error, success, warning, info)
 * - Automatic dismissal handling
 * - Singleton pattern for consistent behavior
 * - Promise-based API for better async handling
 * - Customizable options with sensible defaults
 * - Role-based dismissal tracking
 *
 * @since 1.0.0
 * @version 1.0.0
 */
export class NgxToast implements IDecafToast {
  /**
   * @description Merged toast options used when creating overlays.
   * @summary Holds the current configuration base that each toast operation reuses before
   * applying action-specific overrides such as color or duration.
   * @type {DecafToastOptions}
   */
  options: DecafToastOptions;

  /**
   * @description Creates a toast helper instance.
   * @summary Merges the provided options with the default toast configuration so all
   * subsequent notifications share the same baseline settings.
   * @param {Partial<ToastOptions>} [options={}] - Custom toast options to merge with defaults
   */
  constructor(options: Partial<ToastOptions> = {}) {
    this.options = Object.assign(DefaulOptions, options as DecafToastOptions);
  }

  /**
   * @description Displays an error toast.
   * @summary Presents a toast using the danger color and returns the dismissal role so
   * callers can react to user interaction after the toast closes.
   * @param {string} message - The error message to display
   * @returns {Promise<DecafToastRole>} Promise that resolves with the dismissal role
   * @example
   * ```typescript
   * const role = await toast.error('Failed to save data');
   * if (role === 'cancel') {
   *   // Handle cancellation
   * }
   * ```
   */
  async error(message: string): Promise<DecafToastRole> {
    (component as HTMLIonToastElement) = await this.create(
      Object.assign(this.options, { message: message, color: 'danger' })
    );
    const { role } = await this.handleDidDismiss(component as HTMLIonToastElement);
    return role;
  }

  /**
   * @description Presents a toast using the supplied options.
   * @summary Creates the Ionic toast element, dismisses any active toast, and presents the
   * new overlay. The `message` argument is merged into the final toast configuration so the
   * displayed text always reflects the caller input.
   * @param {string} message - Toast message to display
   * @param {Partial<ToastOptions>} [options] - Toast configuration options
   * @returns {Promise<void>} Promise that resolves when the toast is presented
   */
  async show(message: string, options?: Partial<ToastOptions>): Promise<void> {
    const config = Object.assign({}, this.options, { duration: 5000 }, options, { message });
    if (component) {
      await component.dismiss();
      component = null;
    }
    component = await this.create(config);
    await component.present();
  }

  /**
   * @description Creates a toast element.
   * @summary Builds and stores the Ionic toast element for later presentation or dismissal.
   * This helper is used internally by the public toast methods.
   * @param {Partial<ToastOptions>} options - Toast configuration options
   * @returns {Promise<HTMLIonToastElement>} Promise that resolves with the toast element
   */
  async create(options: Partial<ToastOptions>): Promise<HTMLIonToastElement> {
    return (component = await toastController.create(options));
  }

  /**
   * @description Displays an informational toast.
   * @summary Presents a neutral message with dark styling and any caller-provided options.
   * @param {string} message - The informational message to display
   * @param {Partial<ToastOptions>} [options={}] - Additional toast configuration
   * @returns {Promise<void>} Promise that resolves when toast is presented
   * @example
   * ```typescript
   * await toast.inform('Data loaded successfully');
   * ```
   */
  async inform(message: string, options: Partial<ToastOptions> = {}): Promise<void> {
    await this.show(message, { ...options, color: 'dark' });
  }

  /**
   * @description Displays a success toast.
   * @summary Presents a toast with success styling to confirm positive outcomes or completed
   * operations.
   * @param {string} message - The success message to display
   * @returns {Promise<void>} Promise that resolves when toast is presented
   * @example
   * ```typescript
   * await toast.success('Operation completed successfully!');
   * ```
   */
  async success(message: string): Promise<void> {
    await this.show(message, { color: 'success' });
  }

  /**
   * @description Displays a warning toast.
   * @summary Presents a toast with warning styling and returns the dismissal role after
   * the user closes it.
   * @param {string} message - The warning message to display
   * @returns {Promise<DecafToastRole>} Promise that resolves with the dismissal role
   * @example
   * ```typescript
   * const role = await toast.warn('This action cannot be undone');
   * if (role !== 'cancel') {
   *   // Proceed with action
   * }
   * ```
   */
  async warn(message: string): Promise<DecafToastRole> {
    (component as HTMLIonToastElement) = await this.create(Object.assign(this.options, { color: 'warning', message }));
    const { role } = await this.handleDidDismiss(component as HTMLIonToastElement);
    return role;
  }

  /**
   * @description Resolves the toast dismissal event.
   * @summary Waits for the Ionic dismissal promise, clears the cached toast element, and
   * returns the overlay event detail used by the public methods.
   * @param {HTMLIonToastElement} toast - The toast element to handle dismissal for
   * @returns {Promise<OverlayEventDetail>} Promise that resolves with dismissal event data
   * @private
   */
  private async handleDidDismiss(toast: HTMLIonToastElement): Promise<OverlayEventDetail> {
    const didDismiss = await toast.onDidDismiss();
    component = null;
    return didDismiss as OverlayEventDetail;
  }
}

/**
 * @description Returns the shared toast helper instance.
 * @summary Lazily creates the singleton on first call and reuses it for subsequent toast
 * operations. Any options passed after the first call are ignored because the instance
 * has already been initialized.
 * @function getNgxToastComponent
 * @param {Partial<ToastOptions>} [options={}] - Optional toast configuration options
 * @returns {NgxToast} Singleton NgxToast instance
 * @example
 * ```typescript
 * // Get default instance
 * const toast = getNgxToastComponent();
 *
 * // Get instance with custom options
 * const toast = getNgxToastComponent({
 *   duration: 5000,
 *   position: 'bottom',
 *   animated: false
 * });
 *
 * // All subsequent calls return the same instance
 * const sameToast = getNgxToastComponent(); // Returns existing instance
 * ```
 * @memberOf utils.toast
 */
export function getNgxToast(options: Partial<ToastOptions> = {}): NgxToast {
  if (!instance) {
    instance = new NgxToast(options);
  }
  return instance;
}