Source

lib/components/empty-state/empty-state.component.ts

/**
 * @module module:lib/components/empty-state/empty-state.component
 * @description Empty state component module.
 * @summary Exposes `EmptyStateComponent` which displays a standardized empty
 * state UI with optional icon, title, subtitle and action button. Supports
 * localization and sanitized HTML for dynamic subtitles.
 *
 * @link {@link EmptyStateComponent}
 */

import { Component, inject, Input, OnInit  } from '@angular/core';
import { IonButton } from '@ionic/angular/standalone';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TranslatePipe } from '@ngx-translate/core';
import { Dynamic } from '../../engine/decorators';
import { ElementSizes } from '../../engine/constants';
import { ElementSize, StringOrBoolean } from '../../engine/types';
import { stringToBoolean } from '../../utils/helpers';
import { FunctionLike } from '../../engine/types';
import { CardComponent } from '../card/card.component';
import { IconComponent } from '../../components/icon/icon.component';


/**
 * @description Component for displaying empty state messages with optional actions.
 * @summary This component provides a standardized way to display empty state messages
 * when no data is available or when a user needs to take an action to populate content.
 * It includes customizable title, subtitle, icon, and action button elements that can be
 * styled and configured through input properties. The component supports localization
 * and can trigger navigation or custom actions when the button is clicked.
 *
 * @mermaid
 * classDiagram
 *   class EmptyStateComponent {
 *     +string title
 *     +string titleColor
 *     +string subtitle
 *     +string subtitleColor
 *     +StringOrBoolean showIcon
 *     +string icon
 *     +string iconSize
 *     +string iconColor
 *     +string|Function buttonLink
 *     +string buttonText
 *     +string buttonFill
 *     +string buttonColor
 *     +string buttonSize
 *     +string searchValue
 *     -Router Router
 *     +ngOnInit()
 *     +handleClick()
 *   }
 *   EmptyStateComponent --|> NgxBaseComponentDirective
 *   EmptyStateComponent --|> OnInit
 *
 * @extends {NgxBaseComponentDirective}
 * @implements {OnInit}
 */
@Dynamic()
@Component({
  selector: 'ngx-decaf-empty-state',
  templateUrl: './empty-state.component.html',
  styleUrls: ['./empty-state.component.scss'],
  standalone: true,
  imports: [
    IconComponent,
    TranslatePipe,
    IonButton,
    CardComponent
  ]
})
export class EmptyStateComponent extends CardComponent implements OnInit {


    /**
   * @description The main title displayed in the empty state.
   * @summary Specifies the primary message to show in the empty state component.
   * This text is typically used to inform the user about why they're seeing an empty view.
   * If translatable is true, this will be processed through the localization system.
   *
   * @type {string}
   * @default "title"
   * @memberOf EmptyStateComponent
   */
  @Input()
  override title: string = "title";

  /**
   * @description The color of the title text.
   * @summary Specifies the color for the title text using the application's color system.
   * The value should correspond to a color variable defined in the application's theme.
   * The component will automatically prefix this with "color-" to create the CSS class.
   *
   * @type {string}
   * @default 'gray-6'
   * @memberOf EmptyStateComponent
   */
  @Input()
  titleColor: string = 'gray-6';

  /**
   * @description The secondary message displayed in the empty state.
   * @summary Provides additional context or instructions below the main title.
   * This text is typically used to guide the user on what actions they can take.
   * If translatable is true, this will be processed through the localization system.
   *
   * @type {string | undefined}
   * @memberOf EmptyStateComponent
   */
  @Input()
  override subtitle: string = "";


  /**
   * @description The color of the subtitle text.
   * @summary Specifies the color for the subtitle text using the application's color system.
   * The value should correspond to a color variable defined in the application's theme.
   * The component will automatically prefix this with "color-" to create the CSS class.
   *
   * @type {string}
   * @default 'gray-4'
   * @memberOf EmptyStateComponent
   */
  @Input()
  subtitleColor: string = 'gray-4';

  /**
   * @description Controls whether the icon is displayed.
   * @summary Determines if the visual icon should be shown in the empty state.
   * This can be provided as a boolean or a string that will be converted to a boolean.
   * Icons help visually communicate the empty state context to users.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf EmptyStateComponent
   */
  @Input()
  showIcon: StringOrBoolean = true;

  /**
   * @description The name of the icon to display.
   * @summary Specifies which icon to show when showIcon is true.
   * The component uses the icon system defined in the application,
   * and this value should correspond to an available icon name.
   *
   * @type {string}
   * @default "folder-open-outline"
   * @memberOf EmptyStateComponent
   */
  @Input()
  icon: string = "folder-open-outline";

  /**
   * @description The size of the displayed icon.
   * @summary Controls the size of the icon shown in the empty state.
   * Can be either 'large' or 'small' to accommodate different layout needs.
   *
   * @type {'large' | 'small' | undefined}
   * @default 'large'
   * @memberOf EmptyStateComponent
   */
  @Input()
  iconSize?: Extract<ElementSize, 'large' | 'small'> = ElementSizes.large;

  /**
   * @description The color of the displayed icon.
   * @summary Specifies the color for the icon using Ionic's predefined color system.
   * This allows the icon to match the application's color scheme.
   *
   * @type {string}
   * @default 'medium'
   * @memberOf EmptyStateComponent
   */
  @Input()
  iconColor?: string = 'medium';

  /**
   * @description The navigation target or action for the button.
   * @summary Specifies where the button should navigate to when clicked or what function
   * it should execute. This can be either a URL string or a function that handles navigation.
   * When not provided, the button will not perform any action.
   *
   * @type {string | FunctionLike | undefined}
   * @memberOf EmptyStateComponent
   */
  @Input()
  buttonLink?: string | FunctionLike;

  /**
   * @description The text displayed on the action button.
   * @summary Specifies the label for the action button in the empty state.
   * If translatable is true, this will be processed through the localization system.
   * If not provided, the button will not display any text.
   *
   * @type {string | undefined}
   * @memberOf EmptyStateComponent
   */
  @Input()
  buttonText?: string;

  /**
   * @description The fill style of the action button.
   * @summary Controls the visual style of the button using Ionic's button fill options.
   * 'solid' creates a button with a solid background, 'outline' creates a button with
   * just a border, and 'clear' creates a button with no background or border.
   *
   * @type {'clear' | 'solid' | 'outline'}
   * @default 'solid'
   * @memberOf EmptyStateComponent
   */
  @Input()
  buttonFill: 'clear' | 'solid' | 'outline' =  'solid';

  /**
   * @description The color of the action button.
   * @summary Specifies the color for the button using Ionic's color system.
   * This allows the button to match the application's color scheme.
   *
   * @type {string}
   * @default 'primary'
   * @memberOf EmptyStateComponent
   */
  @Input()
  buttonColor: string =  'primary';

  /**
   * @description The size of the action button.
   * @summary Controls the size of the button shown in the empty state.
   * Can be 'large', 'small', or 'default' to accommodate different layout needs.
   *
   * @type {'large' | 'small' | 'default'}
   * @default 'default'
   * @memberOf EmptyStateComponent
   */
  @Input()
  buttonSize: Extract<ElementSize, 'large' | 'small' | 'default'> =  'default';

  /**
   * @description The search value that resulted in no results.
   * @summary When the empty state is shown due to a search with no results,
   * this property can hold the search term that was used. This can be displayed
   * in the empty state message to provide context to the user.
   *
   * @type {string}
   * @memberOf EmptyStateComponent
   */
  @Input()
  searchValue!: string;


  /**
   * @description Sanitizer instance for bypassing security and sanitizing HTML content.
   * @summary Used to sanitize dynamic HTML content, ensuring it is safe to render in the DOM.
   * @type {DomSanitizer}
   * @memberOf EmptyStateComponent
   */
  private sanitizer: DomSanitizer = inject(DomSanitizer);

  /**
   * @description The sanitized subtitle for search results.
   * @summary Holds the processed and sanitized HTML content for the subtitle when a search yields no results.
   * @type {SafeHtml}
   * @memberOf EmptyStateComponent
   */
  searchSubtitle!: SafeHtml;

  /**
   * @description Flag to enable creation by model route.
   * @summary Indicates whether the component should allow creation of new items via a model route when no button link is provided.
   * @type {boolean}
   * @default false
   * @memberOf EmptyStateComponent
   */
  enableCreationByModelRoute: boolean = false;


  /**
   * @description Creates an instance of EmptyStateComponent.
   * @summary Initializes a new EmptyStateComponent by calling the parent class constructor
   * with the component name for logging and identification purposes. This component provides
   * a standardized way to display empty state messages with optional icons and action buttons.
   *
   * @memberOf EmptyStateComponent
   */
  constructor() {
    super("EmptyStateComponent");
    this.type = 'clear';
    this.componentName = "EmptyStateComponent";
    this.enableDarkMode = true;
  }

  /**
   * @description Initializes the component after Angular first displays the data-bound properties.
   * @summary Sets up the component by processing boolean inputs, applying localization to text
   * elements if translation is enabled, and formatting CSS classes for title and subtitle colors.
   * This method prepares the component for user interaction by ensuring all properties are
   * properly initialized and localized.
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular Lifecycle
   *   participant E as EmptyStateComponent
   *
   *   A->>E: ngOnInit()
   *   E->>E: Process translatable flag
   *   E->>E: Process showIcon flag
   *   E->>E: Get locale settings
   *   alt translatable is true
   *     E->>E: Localize title
   *     E->>E: Localize subtitle
   *     E->>E: Localize buttonText
   *   end
   *   E->>E: Format title CSS class
   *   E->>E: Format subtitle CSS class
   *
   * @return {Promise<void>}
   * @memberOf EmptyStateComponent
   */
  override async ngOnInit(): Promise<void> {
    super.ngOnInit();
    this.showIcon = stringToBoolean(this.showIcon);
    this.titleColor = `dcf-title dcf-color-${this.titleColor}`;
    this.subtitleColor = `dcf-subtitle dcf-color-${this.subtitleColor}`;
    if (this.searchValue)
      this.searchSubtitle = await this.getSearchSubtitle(this.subtitle as string);
    if (!this.buttonLink && this.model && this.route)
      this.enableCreationByModelRoute = true;
  }

  /**
   * @description Handles click events on the action button.
   * @summary This method is triggered when the user clicks the action button in the empty state
   * component. It supports three navigation patterns: 1) no action when buttonLink is not provided,
   * 2) custom function execution when buttonLink is a function, and 3) navigation to a specific URL
   * when buttonLink is a string. This flexibility allows the empty state to trigger various actions
   * based on the context in which it's used.
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant E as EmptyStateComponent
   *   participant N as Router
   *
   *   U->>E: Click action button
   *   E->>E: handleClick()
   *   alt buttonLink is not provided
   *     E-->>U: Return false (no action)
   *   else buttonLink is a function
   *     E->>E: Execute buttonLink function
   *     E-->>U: Return function result
   *   else buttonLink is a URL string
   *     E->>N: navigateForward(buttonLink)
   *     N-->>E: Return navigation result
   *     E-->>U: Return navigation result
   *   end
   *
   * @return {boolean | void | Promise<boolean> | FunctionLike}
   *   - false if no action is taken
   *   - The result of the buttonLink function if it's a function
   *   - A Promise resolving to the navigation result if buttonLink is a URL
   * @memberOf EmptyStateComponent
   */
  handleClick(): boolean | void | Promise<boolean> | FunctionLike {
    const fn = this.buttonLink;
    if (!fn)
      return false;
    if (fn instanceof Function)
      return fn() as FunctionLike;
    return this.router.navigate([fn as string]);
  }


 /**
   * @description Generates a localized and sanitized subtitle for search results.
   * @summary This method takes a content string, typically the subtitle, and processes it
   * through the translation service. It replaces a placeholder ('0') with the actual
   * search value, then sanitizes the result to safely use as HTML. This is particularly
   * useful for displaying dynamic, localized messages in the empty state when a search
   * yields no results.
   *
   * @param {string} content - The content string to be translated and processed
   * @return {Promise<SafeHtml>} A promise that resolves to a sanitized HTML string
   *
   * @mermaid
   * sequenceDiagram
   *   participant E as EmptyStateComponent
   *   participant T as TranslateService
   *   participant S as DomSanitizer
   *
   *   E->>T: instant(content, {'0': searchValue})
   *   T-->>E: Return translated string
   *   E->>S: bypassSecurityTrustHtml(translatedString)
   *   S-->>E: Return sanitized SafeHtml
   *
   * @memberOf EmptyStateComponent
   */
  async getSearchSubtitle(content: string): Promise<SafeHtml> {
    const result = await this.translate(content, {'0': this.searchValue});
    return this.sanitizer.bypassSecurityTrustHtml(result);
  }
}