Source

ui/DecafComponent.ts

import { LoggedClass } from "@decaf-ts/logging";
import { CrudOperationKeys, UIEventProperty, UIFunctionLike } from "./types";
import { Model } from "@decaf-ts/decorator-validation";
import { IRepository, OperationKeys } from "@decaf-ts/db-decorators";
import { Constructor } from "@decaf-ts/decoration";
import { DecafEventHandler } from "./DecafEventHandler";

type PrimaryKeyType = string | number | bigint;

export const isClassConstructor = <C>(
  value: UIFunctionLike | Constructor<C>
): value is Constructor<C> => {
  return typeof value === "function" && /^class\s/.test(String(value));
};

/**
 * Base class for all Decaf UI components, providing common state management,
 * logging, localization, navigation hooks, CRUD context metadata, and
 * repository integration used by higher-level decorators and renderers.
 */
export abstract class DecafComponent<M extends Model> extends LoggedClass {
  /**
   * @description Data model or model name for component operations.
   * @summary The data model that this component will use for CRUD operations. This can be provided
   * as a Model instance, a model constructor, or a string representing the model's registered name.
   * When set, this property provides the component with access to the model's schema, validation rules,
   * and metadata needed for rendering and data operations.
   * @type {M | Model | string | undefined}
   */
  model!: M | Model | string | undefined;

  /**
   * @description Primary key value of the current model instance.
   * @summary Specifies the primary key value for the current model record being displayed or
   * manipulated by the component. This identifier is used for CRUD operations that target
   * specific records, such as read, update, and delete operations. The value corresponds to
   * the field designated as the primary key in the model definition.
   * @type {PrimaryKeyType | PrimaryKeyType[]}
   */
  modelId?: PrimaryKeyType | PrimaryKeyType[];

  /**
   * @description Pre-built filtering expression applied to repository queries.
   * @summary Supply a custom `AttributeOption` to control how records are constrained. When omitted,
   * the directive derives a condition from `filterBy` or `pk`, comparing it against `modelId`.
   * @type {any}
   */
  filter!: any;

  /**
   * @description Model field used when generating the default condition.
   * @summary Indicates which key should be compared to `modelId` when `filter` is not provided.
   * Defaults to the configured primary key so overrides are only needed for custom lookups.
   * @type {string}
   */
  filterBy!: string;

  /**
   * @description The CRUD operation type to be performed on the model.
   * @summary Specifies which operation (Create, Read, Update, Delete) this component instance
   * should perform. This determines the UI behavior, form configuration, and available actions.
   * The operation affects form validation, field availability, and the specific repository
   * method called during data submission.
   *
   * @type {OperationKeys}
   */
  operation?: OperationKeys;

  /**
   * @description Router instance for programmatic navigation.
   * @summary Injected Router service used for programmatic navigation between routes
   * in the application. This service enables navigation to different views and operations,
   * handles route parameters, and manages the browser's navigation history.
   * @protected
   * @type {Router}
   */
  router?: any;

  /**
   * @description Name identifier for the component instance.
   * @summary Provides a string identifier that can be used to name or label the component
   * instance. This name can be used for debugging purposes, logging, or to identify specific
   * component instances within a larger application structure. It serves as a human-readable
   * identifier that helps distinguish between multiple instances of the same component type.
   * @type {string}
   */
  name!: string;

  /**
   * @description Parent component identifier for hierarchical component relationships.
   * @summary Specifies the identifier of the parent component in a hierarchical component structure.
   * This property establishes a parent-child relationship between components, allowing for
   * proper nesting and organization of components within a layout. It can be used to track
   * component dependencies and establish component hierarchies for rendering and event propagation.
   * @type {string | undefined}
   */
  childOf!: string | undefined;

  /**
   * @description Unique identifier for the component instance.
   * @summary A unique identifier automatically generated for each component instance.
   * This UID is used for DOM element identification, component tracking, and debugging purposes.
   * By default, it generates a random 16-character value, but it can be explicitly set via input.
   * @type {string}
   */
  uid?: PrimaryKeyType;

  /**
   * @description Primary key field name for the data model.
   * @summary Specifies which field in the model should be used as the primary key.
   * This is typically used for identifying unique records in operations like update and delete.
   * If not explicitly set, it defaults to the repository's configured primary key or 'id'.
   * @type {string}
1   */
  pk!: string;

  /**
   * @description Angular change detection service for manual change detection control.
   * @summary Injected service that provides manual control over change detection cycles.
   * This is essential for ensuring that programmatic DOM changes (like setting accordion
   * attributes) are properly reflected in the component's state and trigger appropriate
   * view updates when modifications occur outside the normal Angular change detection flow.
   * @protected
   */
  protected changeDetectorRef?: any;

  /**
   * @description Controls field visibility based on CRUD operations.
   * @summary Can be a boolean or an array of operation keys where the field should be hidden.
   * @type {boolean | CrudOperationKeys[]}
   */
  hidden?: boolean | CrudOperationKeys[];

  /**
   * @description Label for the file upload field.
   * @summary Provides a user-friendly label for the file upload input.
   *
   * @type {string | undefined}
   */
  label?: string;

  /**
   * @description Whether the field is read-only.
   * @type {boolean}
   * @public
   */
  readonly?: boolean;

  /**
   * @description Flag to enable or disable dark mode support for the component.
   * @summary When enabled, the component will automatically detect the system's dark mode
   * preference using the media service and apply appropriate styling classes. This flag
   * controls whether the component should respond to dark mode changes and apply the
   * dark palette class to its DOM element. By default, dark mode support is disabled.
   * @protected
   * @type {boolean}
   * @default false
   */
  protected enableDarkMode: boolean = true;

  /**
   * @description Flag to enable or disable dark mode support for the component.
   * @summary When enabled, the component will automatically detect the system's dark mode
   * preference using the media service and apply appropriate styling classes. This flag
   * controls whether the component should respond to dark mode changes and apply the
   * dark palette class to its DOM element. By default, dark mode support is disabled.
   * @protected
   * @type {boolean}
   * @default false
   */
  protected isDarkMode: boolean = false;

  /**
   * @description Current locale identifier for component internationalization.
   * @summary Specifies the locale code (e.g., 'en-US', 'pt-BR') used for translating UI text
   * and formatting data according to regional conventions. This property can be set to override
   * the default application locale for this specific component instance, enabling per-component
   * localization when needed.
   * @type {string | undefined}
   */
  locale?: string;

  /**
   * @description Configuration for list item rendering behavior.
   * @summary Defines how list items should be rendered in the component.
   * This property holds a configuration object that specifies the tag name
   * and other properties needed to render list items correctly. The tag property
   * identifies which component should be used to render each item in a list.
   * Additional properties can be included to customize the rendering behavior.
   * @type {Record<string, unknown>}
   * @default {tag: ""}
   */
  item: Record<string, unknown> = { tag: "" };

  /**
   * @description Dynamic properties configuration for runtime customization.
   * @summary Contains key-value pairs of dynamic properties that can be applied to the component
   * at runtime. This flexible configuration object allows for dynamic property assignment without
   * requiring explicit input bindings for every possible configuration option. Properties from
   * this object are parsed and applied to the component instance through the parseProps method,
   * enabling customizable component behavior based on external configuration.
   * @type {Record<string, unknown>}
   * @default {}
   */
  props: Record<string, unknown> = {};

  /**
   * @description Base route path for component navigation.
   * @summary Defines the base route path used for navigation actions related to this component.
   * This is often used as a prefix for constructing navigation URLs when transitioning between
   * different operations or views. The route helps establish the component's position in the
   * application's routing hierarchy.
   * @type {string}
   */
  route?: string = "";

  /**
   * @description Controls whether borders are displayed around the component.
   * @summary Boolean flag that determines if the component should be visually outlined with borders.
   * When true, borders are shown to visually separate the component from surrounding content.
   *
   * @type {boolean}
   * @default false
   */
  borders: boolean = false;

  /**
   * @description Component name identifier for logging and localization contexts.
   * @summary Stores the component's name which is used as a key for logging contexts
   * and as a base for locale resolution.
   * @protected
   * @type {string | undefined}
   */
  protected componentName?: string;

  /**
   * @description Root key for component locale context resolution.
   * @summary Defines the base key used to resolve localization contexts for this component.
   * If not explicitly provided, it defaults to the component's name. This key is used to
   * load appropriate translation resources and locale-specific configurations.
   * @protected
   * @type {string | undefined}
   */
  protected localeRoot?: string;

  /**
   * @description Current value of the component.
   * @summary Can be a string, number, date, or array of string or objects.
   * @type {any}
   * @public
   */
  value?: any;

  /**
   * @description Reference to CRUD operation constants for template usage.
   * @summary Exposes the OperationKeys enum to the component template, enabling
   * conditional rendering and behavior based on operation types. This protected
   * readonly property ensures that template logic can access operation constants
   * while maintaining encapsulation and preventing accidental modification.
   * @protected
   * @readonly
   */
  protected readonly OperationKeys = OperationKeys;

  /**
   * @description Angular Location service.
   * @summary Injected service that provides direct access to the browser's URL and history.
   * Unlike the Router, Location allows for low-level manipulation of the browser's history stack
   * and URL path, such as programmatically navigating back or forward, or updating the URL without
   * triggering a route change. This is useful for scenarios where you need to interact with the
   * browser history or URL outside of Angular's routing system, such as closing modals, handling
   * popstate events, or supporting custom navigation logic.
   *
   */
  location!: any;

  /**
   * @description Repository instance for data layer operations.
   * @summary Provides a connection to the data layer for retrieving and manipulating data.
   * This is an instance of the DecafRepository class, initialized lazily in the repository getter.
   * The repository is used to perform CRUD (Create, Read, Update, Delete) operations on the
   * data model and provides methods for querying and filtering data based on specific criteria.
   * @type {IRepository<M>}
   * @protected
   */
  protected _repository?: IRepository<M>;

  /**
   * @description Initialization status flag for the component.
   * @summary Tracks whether the component has completed its initialization process.
   * This flag is used to prevent duplicate initialization and to determine if
   * certain operations that require initialization can be performed.
   * @type {boolean}
   * @default false
   */
  protected initialized: boolean = false;

  protected events: UIEventProperty = {};

  protected handlers: UIEventProperty = {};

  constructor() {
    super();
  }

  get repository(): IRepository<M> {
    return this._repository as IRepository<M>;
  }

  set repository(repository: IRepository<M>) {
    this._repository = repository;
  }

  async render(...args: unknown[]): Promise<any> {
    this.log
      .for(this.render)
      .info(`render for ${this.componentName} with ${JSON.stringify(args)}`);
  }

  async refresh(...args: unknown[]): Promise<any> {
    this.log.for(this.refresh).info(`Refresh called with args: ${args}`);
  }

  /**
   * Asynchronously initializes the component with the provided arguments.
   * This method sets the `initialized` property to `true` once the initialization is complete.
   *
   * @param args - A variable number of arguments of unknown types that can be used for initialization.
   * @returns A promise that resolves when the initialization is complete.
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async initialize(...args: unknown[]): Promise<any> {
    this.initialized = true;
  }

  /**
   * Translates content based on the provided arguments.
   * Logs the translation request with the component name and arguments.
   *
   * @param args - A variable number of arguments used for translation.
   * @returns A promise that resolves with the translation result.
   */
  async translate(...args: unknown[]): Promise<any> {
    this.log
      .for(this.translate)
      .info(`translate for ${this.componentName} with ${JSON.stringify(args)}`);
  }

  async preview(...args: unknown[]): Promise<void> {
    this.log.for(this.preview).debug(`Preview called with args: ${args}`);
  }

  // async process(...args: unknown[]): Promise<any> {
  //   this.log.for(this.process).debug(`Process called with args: ${args}`);
  // }

  // async batchOperation(...args: unknown[]): Promise<any> {
  //   this.log
  //     .for(this.batchOperation)
  //     .debug(`BatchOperation called with args: ${args}`);
  // }

  /**
   * Submits data or performs an action associated with the component.
   *
   * @param args - A variable number of arguments of any type to be passed to the submit operation.
   * @returns A promise that resolves with the result of the submit operation.
   */
  async submit(...args: unknown[]): Promise<any> {
    this.log
      .for(this.submit)
      .info(`submit for ${this.componentName} with ${JSON.stringify(args)}`);
  }

  /**
   * @description Normalizes handler definitions into executable functions.
   * @summary Iterates through a handlers map and ensures each entry is stored as a callable
   * function on the component. Plain functions are stored directly, while handler classes are
   * instantiated so their `handle` method (or a keyed override) becomes the registered handler.
   * This allows decorators to accept either inline functions or handler classes transparently.
   * @template T Extends `DecafEventHandler` to constrain handler class types.
   * @param handlers - Dictionary of handler names mapped to functions or handler constructors.
   * @param instance - Optional target handler instance; defaults to the current component.
   */
  protected parseHandlers<T extends DecafEventHandler>(
    handlers: UIEventProperty,
    instance: T
  ): UIEventProperty {
    const result: UIEventProperty = {};
    Object.entries(handlers).forEach(([key, fn]) => {
      if (isClassConstructor<DecafEventHandler>(fn)) {
        const clazz = new fn() as DecafEventHandler;
        const event =
          key in clazz ? clazz[key as keyof DecafEventHandler] : clazz.handle;
        result[key] = event;
        instance.handlers[key] = event;
      } else {
        result[key] = fn;
        instance.handlers[key] = fn;
      }
    });
    return result;
  }

  /**
   * @description Registers event callbacks on the component instance.
   * @summary Processes an events map produced by decorators where each value can be a factory
   * returning a function or a component constructor. When a constructor is returned, the method
   * matching the event key is instantiated and stored; otherwise the produced function is
   * assigned directly. This enables flexible event binding definitions for derived components.
   * @template T Extends `DecafComponent<Model>` to scope acceptable event owners.
   * @param events - Dictionary of event names mapped to function factories or component constructors.
   * @param instance - Optional component instance to receive the events; defaults to `this`.
   */
  protected parseEvents<T extends DecafComponent<Model>>(
    events: UIEventProperty,
    instance: T
  ): UIEventProperty {
    const result: UIEventProperty = {};
    Object.entries(events).forEach(([key, fn]) => {
      const name = key as string;
      const evt = (fn as UIFunctionLike)();
      if (isClassConstructor<T>(evt)) {
        const fn = new evt()[key as keyof T] as UIFunctionLike;
        if (fn) {
          result[name] = fn;
          instance.events[name] = fn;
        }
      } else {
        result[name] = fn;
        instance.events[name] = fn;
      }
    });
    return result;
  }
}