Source

lib/engine/NgxComponentDirective.ts

/**
 * @module lib/engine/NgxComponentDirective
 * @description Base decaf component abstraction providing shared inputs and utilities.
 * @summary NgxComponentDirective is the abstract foundation for Decaf components and provides common
 * inputs (model, mapper, pk, props), logging, repository resolution, and event dispatch helpers.
 * It centralizes shared behavior for child components and simplifies integration with the rendering engine.
 * @link {@link NgxComponentDirective}
 */
import {
  Directive,
  ElementRef,
  EventEmitter,
  Inject,
  inject,
  Input,
  Output,
  SimpleChanges,
  ViewChild,
  OnChanges,
  ChangeDetectorRef,
  Renderer2,
  OnDestroy,
  signal,
  WritableSignal,
  EnvironmentInjector,
} from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { firstValueFrom, shareReplay, Subject, takeUntil } from 'rxjs';
import { Model, ModelConstructor, ModelKeys, Primitives } from '@decaf-ts/decorator-validation';
import { CrudOperations, InternalError, OperationKeys } from '@decaf-ts/db-decorators';
import { ComponentEventNames, DecafEventHandler } from '@decaf-ts/ui-decorators';
import {
  DecafRepository,
  FormParent,
  FunctionLike,
  KeyValue,
  PropsMapperFn,
  WindowColorScheme,
} from './types';
import { IBaseCustomEvent, ICrudFormEvent } from './interfaces';
import {} from './NgxEventHandler';
import { getLocaleContext } from '../i18n/Loader';
import { NgxRenderingEngine } from './NgxRenderingEngine';
import { AngularEngineKeys, BaseComponentProps, CPTKN, WindowColorSchemes } from './constants';
import { generateRandomValue, getWindow, setOnWindow } from '../utils';
import { NgxMediaService } from '../services/NgxMediaService';
import { UIFunctionLike, UIKeys } from '@decaf-ts/ui-decorators';
import { LoadingController, LoadingOptions } from '@ionic/angular/standalone';
import { OverlayBaseController } from '@ionic/angular/common';
import { getModelAndRepository } from './helpers';
import { NgxRepositoryDirective } from './NgxRepositoryDirective';

try {
  const win = getWindow();
  if (!win?.[AngularEngineKeys.LOADED]) new NgxRenderingEngine();
  setOnWindow(AngularEngineKeys.LOADED, true);
} catch (e: unknown) {
  throw new Error(`Failed to load rendering engine: ${e}`);
}

/**
 * @description Base directive for Decaf components in Angular applications.
 * @summary Abstract base class that provides common functionality for all Decaf components.
 * This directive establishes a foundation for component development by offering shared inputs
 * (model, mapper, pk, props), logging infrastructure, repository access, event handling, and
 * internationalization support. It implements OnChanges to respond to input property changes
 * and includes utilities for navigation, localization, and dynamic property binding. All Decaf
 * components should extend this directive to inherit its foundational capabilities.
 * @class NgxComponentDirective
 * @extends {LoggedClass}
 * @implements {OnChanges}
 * @memberOf module:lib/engine/NgxComponentDirective
 */
@Directive({ host: { '[attr.id]': 'uid' } })
export abstract class NgxComponentDirective
  extends NgxRepositoryDirective<Model>
  implements OnChanges, OnDestroy
{
  /**
   * @description Reference to the component's native DOM element.
   * @summary Provides direct access to the native DOM element of the component through Angular's
   * ViewChild decorator. This reference can be used to manipulate the DOM element directly,
   * apply custom styles, or access native element properties and methods. The element is
   * identified by the 'component' template reference variable.
   * @type {ElementRef}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @ViewChild('component', { read: ElementRef, static: true })
  component!: ElementRef;

  /**
   * @description Writable signal used for prop synchronization.
   * @summary Stores a reactive snapshot of component props so derived classes
   * can observe assignments performed by `initProps` without directly mutating
   * the instance. Signals ensure Angular change detection reacts to prop updates.
   * @type {WritableSignal<NgxComponentDirective>}
   */
  _props: WritableSignal<NgxComponentDirective> = signal<NgxComponentDirective>(
    {} as NgxComponentDirective,
  );

  /**
   * @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
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  protected override 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
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  protected override isDarkMode: boolean = false;

  /**
   * @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}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override 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}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override childOf: string = '';

  /**
   * @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 | number}
   * @default generateRandomValue(16)
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override uid?: string | number;

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

  /**
   * @description Query predicate applied when resolving model data.
   * @summary Provides an optional set of conditions used to filter the repository query that
   * supplies data to this component. When present, the condition constrains the model lookup
   * (e.g., WHERE clauses) so only records matching the specified criteria are fetched, enabling
   * contextualized reads such as filtering by status, tenant, or foreign keys. This is especially
   * useful when the component should render or operate on a subset of records rather than a single
   * primary-key match.
   * @type {AttributeOption<Model> | undefined}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  // @Input()
  // condition: AttributeOption<Model> | undefined;

  /**
   * @description Angular reactive FormGroup for form state management.
   * @summary The FormGroup instance that manages all form controls, validation,
   * and form state. This is the main interface for accessing form values and
   * controlling form behavior. May be undefined for read-only operations.
   *
   * @type {FormGroup | undefined}
   */
  @Input()
  formGroup: FormParent | undefined = undefined;

  /**
   * @description Backing value supplied to the component instance.
   * @summary Holds the data payload bound to the component (for example, the field value in a
   * form control or the record currently being edited). The directive treats this input as an
   * opaque value and passes it through to child components or handlers, allowing each component
   * implementation to interpret the value according to its own semantics (e.g., scalar, object,
   * or collection). This is not tied to any primary key; it simply mirrors whatever data the
   * host template provides.
   * @type {unknown}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override value: unknown;

  /**
   * @description Field mapping configuration object or function.
   * @summary Defines how fields from the data model should be mapped to properties used by the component.
   * This allows for flexible data binding between the model and the component's display logic. Can be
   * provided as a static object mapping or as a function for dynamic mapping transformations.
   * @type {Record<string, string> | FunctionLike | Record<string, FunctionLike>}
   * @default {}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  mapper: Record<string, string> | FunctionLike | Record<string, FunctionLike> = {};

  /**
   * @description Available CRUD operations for this component instance.
   * @summary Defines which CRUD operations (Create, Read, Update, Delete) are available
   * for this component. This affects which operations can be performed on the data and
   * which operation buttons are displayed in the UI. By default, only READ operations are enabled.
   * @type {CrudOperations[]}
   * @default [OperationKeys.READ]
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  operations: CrudOperations[] = [OperationKeys.READ];

  /**
   * @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}
   * @default OperationKeys.READ
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override operation: OperationKeys | undefined = OperationKeys.READ;

  /**
   * @description Row position in a grid-based layout system.
   * @summary Specifies the row position of this component when rendered within a grid-based layout.
   * This property is used for positioning components in multi-row, multi-column layouts and helps
   * establish the component's vertical placement within the grid structure.
   * @type {number}
   * @default 1
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  row: number = 1;

  /**
   * @description Column position in a grid-based layout system.
   * @summary Specifies the column position of this component when rendered within a grid-based layout.
   * This property is used for positioning components in multi-row, multi-column layouts and helps
   * establish the component's horizontal placement within the grid structure.
   * @type {number}
   * @default 1
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  col: number = 1;

  /**
   * @description Additional CSS class names for component styling.
   * @summary Allows custom CSS classes to be added to the component's root element.
   * These classes are appended to any automatically generated classes based on other
   * component properties. Multiple classes can be provided as a space-separated string.
   * This provides a way to customize the component's appearance beyond the built-in styling options.
   * @type {string}
   * @default ""
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  className: string = '';

  /**
   * @description Translatability of field labels.
   * @summary Indicates whether the field labels should be translated based on the current language settings.
   * This is useful for applications supporting multiple languages.
   *
   * @type {boolean}
   * @default true
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  translatable: boolean = true;

  /**
   * @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
   * @type {ChangeDetectorRef}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected override changeDetectorRef: ChangeDetectorRef = inject(ChangeDetectorRef);

  /**
   * @description Injector used for dependency injection in the dynamic component.
   * @summary This injector is used when creating the dynamic component to provide it with
   * access to the application's dependency injection system. It ensures that the dynamically
   * created component can access the same services and dependencies as statically created
   * components.
   *
   * @type {EnvironmentInjector}
   */
  protected injector: EnvironmentInjector = inject(EnvironmentInjector);

  /**
   * @description Media service instance for responsive design and media query management.
   * @summary Provides access to media query functionality for detecting and responding to
   * different screen sizes and device capabilities. This service enables components to adapt
   * their behavior and presentation based on viewport dimensions, orientation, and other
   * media features. It manages media query listeners and provides utilities for responsive
   * component rendering. The service is instantiated per component and should be destroyed
   * via ngOnDestroy to prevent memory leaks.
   * @protected
   * @type {NgxMediaService}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected mediaService: NgxMediaService = inject(NgxMediaService);

  /**
   * @description Angular Renderer2 service for platform-agnostic DOM manipulation.
   * @summary Injected service that provides a safe, platform-agnostic way to manipulate DOM elements.
   * This service ensures proper handling of DOM operations across different platforms and environments,
   * including server-side rendering and web workers, without direct DOM access.
   * @protected
   * @type {Renderer2}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected renderer: Renderer2 = inject(Renderer2);

  /**
   * @description Translation service for application internationalization.
   * @summary Injected service that provides translation capabilities for UI text.
   * Used to translate button labels, validation messages, and other text content based
   * on the current locale setting, enabling multilingual support throughout the application.
   * @protected
   * @type {TranslateService}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected translateService: TranslateService = inject(TranslateService);

  /**
   * @description Event emitter for custom component events.
   * @summary Emits custom events that occur within child components or the component itself.
   * This allows parent components to listen for and respond to user interactions or
   * state changes. Events are passed up the component hierarchy to enable coordinated
   * behavior across the application.
   * @type {EventEmitter<IBaseCustomEvent>}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Output()
  listenEvent: EventEmitter<IBaseCustomEvent | ICrudFormEvent> = new EventEmitter<
    IBaseCustomEvent | ICrudFormEvent
  >();

  /**
   * @description Event emitter for custom component events.
   * @summary Emits custom events that occur within child components or the component itself.
   * This allows parent components to listen for and respond to user interactions or
   * state changes. Events are passed up the component hierarchy to enable coordinated
   * behavior across the application.
   * @type {EventEmitter<IBaseCustomEvent>}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Output()
  refreshEvent: EventEmitter<IBaseCustomEvent | boolean> = new EventEmitter<
    IBaseCustomEvent | boolean
  >();

  /**
   * @description Angular 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}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  override router: Router = inject(Router);

  /**
   * @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}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override 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, FieldDefinition>}
   * @default {tag: ""}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override 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 {}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override props: Record<string, unknown> | Partial<NgxComponentDirective> = {};

  /**
   * @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}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  override 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
   */
  @Input()
  override borders: boolean = false;

  /**
   * @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.
   *
   * @private
   * @type {Location}
   */
  override location: Location = inject(Location);

  /**
   * @description Ionic loading overlay manager available to derived components.
   * @summary Provides convenient access to Ionic's LoadingController, enabling directive
   * subclasses to create, present, and dismiss loading overlays without wiring the
   * service themselves. Use this helper to surface async progress indicators tied to
   * CRUD operations, navigation, or any long-running UI task.
   * @type {LoadingController}
   * @returns {OverlayBaseController<LoadingOptions, HTMLIonLoadingElement>}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected loadingController: OverlayBaseController<LoadingOptions, HTMLIonLoadingElement> =
    inject(LoadingController);

  /**
   * @description Flag indicating if the component is rendered as a child of a modal dialog.
   * @summary Determines whether this component instance is being displayed within a modal
   * context. This flag affects component behavior such as navigation patterns, event handling,
   * and lifecycle management. When true, the component may use different navigation strategies
   * (e.g., closing the modal instead of browser navigation) and adjust its layout to fit modal
   * constraints. This is typically set by parent modal containers when instantiating child components.
   * @protected
   * @type {boolean}
   * @default false
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  @Input()
  isModalChild: boolean = false;

  @Input()
  protected override handlers: Record<string, UIFunctionLike> = {};

  @Input()
  protected override events: Record<string, UIFunctionLike> = {};

  @Input()
  propsMapperFn!: PropsMapperFn<NgxComponentDirective>;

  /**
   * @description Indicates whether a refresh operation is in progress.
   * @summary When true, the component is currently fetching new data. This is used
   * to control loading indicators and prevent duplicate refresh operations from
   * being triggered simultaneously.
   *
   * @type {boolean}
   * @default false
   */
  refreshing: boolean = false;

  /**
   * @description Represents the color scheme used for application.
   * @summary  This property determines the visual appearance of the application
   * based on predefined color schemes (e.g., light or dark mode).
   * @type {WindowColorScheme}
   * @default WindowColorSchemes.light
   */
  protected colorSchema: WindowColorScheme = WindowColorSchemes.light;

  protected destroySubscriptions$ = new Subject<void>();

  /**
   * @description Constructor for NgxComponentDirective.
   * @summary Initializes the directive by setting up the component name, locale root,
   * and logger. Calls the parent LoggedClass constructor and configures localization
   * context. The component name and locale root can be optionally injected via the
   * CPTKN token, otherwise defaults are used.
   * @param {string} [componentName] - Optional component name for identification and logging
   * @param {string} [localeRoot] - Optional locale root key for internationalization
   * @memberOf module:lib/engine/NgxComponentDirective
   */

  constructor(@Inject(CPTKN) componentName?: string, @Inject(CPTKN) localeRoot?: string) {
    super();
    this.value = undefined;
    this.componentName = componentName || this.constructor.name || 'NgxComponentDirective';
    this.localeRoot = localeRoot;
    if (!this.localeRoot && this.componentName) this.localeRoot = this.componentName;
    if (this.localeRoot) this.getLocale(this.localeRoot);
    this.uid = `${this.componentName}-${generateRandomValue(8)}`;
    this.mediaService.isDarkMode().subscribe((isDark) => {
      if (isDark) {
        this.isDarkMode = isDark;
        this.colorSchema = WindowColorSchemes.dark;
      }
    });
  }

  override async initialize<T extends NgxComponentDirective>(): Promise<void> {
    this.mediaService.darkModeEnabled();
    // connect component to media service for color scheme toggling
    this.mediaService.colorSchemeObserver(this.component);

    if (!this.initialized && Object.keys(this.props || {}).length) {
      this.parseProps(this);
    }

    this.router.events
      .pipe(shareReplay(1), takeUntil(this.destroySubscriptions$))
      .subscribe(async (event) => {
        if (event instanceof NavigationStart) {
          if (this.value) {
            await this.ngOnDestroy();
          }
        }
      });

    this.route = this.router.url.replace('/', '');

    const instance = this as KeyValue;
    if (this.propsMapperFn) {
      for (const [key, fn] of Object.entries(this.propsMapperFn)) {
        if (key in instance) instance[key] = await fn(instance as T);
      }
    }
    // search for handler to render event
    if (!this.initialized) {
      const handler = this.handlers?.[ComponentEventNames.Render] || undefined;
      if (handler && typeof handler === 'function') {
        await handler.bind(instance)(instance as T, this.name, this.value);
      }
      // search for event to render event
      const event = this.events?.[ComponentEventNames.Render] || undefined;
      if (event && typeof event === 'function') {
        await event.bind(instance)(instance as T, this.name, this.value);
      }
    }
    await super.initialize();

    this.initialized = true;
    if (this.isModalChild) {
      this.changeDetectorRef.detectChanges();
    }
  }

  /**
   * @description Cleanup lifecycle hook invoked when the directive is destroyed.
   * @summary Ensures any resources allocated by the directive's media service are
   * released (DOM listeners, timers, subscriptions, etc.). Implementations should
   * keep `mediaService.destroy()` idempotent; calling it here prevents leaks when
   * components are torn down.
   * @returns {Promise<void>}
   */
  async ngOnDestroy(): Promise<void> {
    this.value = undefined;
    this.mediaService.destroy();
    if (this.destroySubscriptions$) {
      this.destroySubscriptions$.next();
      this.destroySubscriptions$.complete();
    }
  }

  /**
   * @description Getter for the current locale context identifier.
   * @summary Returns the current locale identifier by calling the getLocale method.
   * This property provides convenient access to the component's active locale setting.
   * @returns {string} The current locale identifier
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  get localeContext(): string {
    return this.getLocale();
  }

  /**
   * @description Getter for the data repository instance.
   * @summary Lazily initializes and returns the DecafRepository instance for the current model.
   * This getter ensures the repository is created only when needed and reused for subsequent
   * access. It also automatically sets the primary key field if not explicitly configured.
   * @protected
   * @returns {DecafRepository<Model>} The repository instance for the current model
   * @throws {InternalError} If repository initialization fails
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  override get repository(): DecafRepository<Model> {
    try {
      if (!this._repository) {
        const context = getModelAndRepository(this.model as Model);
        if (context) {
          const { repository, pk, pkType } = context;
          this._repository = repository;
          if (this.model && !this.pk) this.pk = pk;
          this.pkType = pkType || Model.pk(repository.class);
          if (!this.modelName) this.modelName = repository.class.name;
        }
      }
    } catch (error: unknown) {
      throw new InternalError((error as Error)?.message || (error as string));
    }
    return this._repository as DecafRepository<Model>;
  }

  override set repository(repository: DecafRepository<Model> | undefined) {
    this._repository = repository;
  }

  /**
   * @description Angular lifecycle hook for handling input property changes.
   * @summary Responds to changes in component input properties, specifically monitoring changes
   * to the model, locale root, and component name properties. When the model changes, it triggers
   * model initialization and locale context updates. When locale-related properties change,
   * it updates the component's locale setting accordingly.
   * @param {SimpleChanges} changes - Object containing the changed properties with their previous and current values
   * @return {void}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  async ngOnChanges(changes: SimpleChanges): Promise<void> {
    if (changes[ModelKeys.MODEL]) {
      const { currentValue } = changes[ModelKeys.MODEL];
      if (currentValue) {
        this.getModel(currentValue);
      }
      this.locale = this.localeContext;
    }

    // if (changes[UIKeys.HANDLERS]) {
    //   const { currentValue, previousValue } = changes[UIKeys.HANDLERS];
    //   if (currentValue && typeof currentValue !== previousValue) {
    //     for(const key in currentValue) {
    //       const event = currentValue[key]();
    //       if (event && typeof event === 'function') {
    //         const clazz = new event();
    //         this.handlers[key] = clazz[key].bind(this);
    //         console.log(this.handlers);
    //       }
    //     }
    //   }
    // }

    if (changes[BaseComponentProps.HANDLERS]) {
      const { currentValue, previousValue } = changes[BaseComponentProps.HANDLERS];
      if (currentValue && currentValue !== previousValue)
        this.parseHandlers(currentValue, this as unknown as DecafEventHandler);
    }

    if (changes[UIKeys.EVENTS]) {
      const { currentValue, previousValue } = changes[UIKeys.EVENTS];
      if (currentValue && currentValue !== previousValue) {
        if (!this._repository) {
          this._repository = this.repository;
        }
        this.parseEvents(currentValue, this);

        // for (const key in currentValue) {
        //   const event = currentValue[key]();
        //   if (event && typeof event === 'function') {
        //     try {
        //       const clazz = new event();
        //       this.events[key] = clazz[key].bind(this);
        //       if (event[key] instanceof Promise) {
        //         await clazz[key].bind(this)();
        //       } else {
        //         clazz[key].bind(this)();
        //       }
        //     } catch (error: unknown) {
        //       this.log
        //         .for(this.ngOnChanges)
        //         .error(
        //           `Error occurred while processing event "${key}": ${(error as Error)?.message || (error as string)}`
        //         );
        //     }
        //   }
        // }
      }
    }
    if (changes[BaseComponentProps.LOCALE_ROOT] || changes[BaseComponentProps.COMPONENT_NAME])
      this.locale = this.localeContext;

    if (this.enableDarkMode) this.checkDarkMode();
  }

  /**
   * @description Translates text phrases using the translation service.
   * @summary Provides a promise-based wrapper around the translation service to translate
   * UI text based on the current locale. Supports both single phrases and arrays of phrases,
   * and accepts optional parameters for template interpolation. When a string parameter is
   * provided, it's automatically converted to an object format for the translation service.
   * @param {string | string[]} phrase - The translation key or array of keys to translate
   * @param {object | string} [params] - Optional parameters for interpolation in translated text
   * @return {Promise<string>} A promise that resolves to the translated text
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  override async translate(phrase: string | string[], params?: object | string): Promise<string> {
    if (!phrase) return '';
    if (typeof params === Primitives.STRING) params = { '0': params };
    return await firstValueFrom(this.translateService.get(phrase, (params || {}) as object));
  }

  protected checkDarkMode(): void {
    this.mediaService.isDarkMode().subscribe((isDark) => {
      this.isDarkMode = isDark;
      this.mediaService.toggleClass(
        [this.component],
        AngularEngineKeys.DARK_PALETTE_CLASS,
        this.isDarkMode,
      );
    });
  }

  // parseHandlers(handlers: Record<string, UIFunctionLike>): void {
  //   // function isClass(value: UIFunctionLike | Constructor<NgxEventHandler>): boolean {
  //   //   return typeof value === 'function' && /^class\s/.test(String(value));
  //   // }
  //   Object.entries(handlers).forEach(([key, fn]) => {
  //     if (isClassConstructor<NgxEventHandler>(fn)) {
  //       const clazz = new fn() as NgxEventHandler;
  //       this.handlers[key] = key in clazz ? clazz[key as keyof NgxEventHandler] : clazz.handle;
  //     } else {
  //       this.handlers[key] = fn as UIFunctionLike;
  //     }
  //   });
  // }

  // for (const key in currentValue) {
  //   const event = currentValue[key]();
  //   if (event && typeof event === 'function') {
  //     try {
  //       const clazz = new event();
  //       this.events[key] = clazz[key].bind(this);
  //       if (event[key] instanceof Promise) {
  //         await clazz[key].bind(this)();
  //       } else {
  //         clazz[key].bind(this)();
  //       }
  //     } catch (error: unknown) {
  //       this.log
  //         .for(this.ngOnChanges)
  //         .error(
  //           `Error occurred while processing event "${key}": ${(error as Error)?.message || (error as string)}`
  //         );
  //     }
  //   }
  // }

  // parseEvents<T extends DecafComponent<Model>>(
  //   events: Record<string, UIFunctionLike>,
  // ): UIEventProperty {
  //   const result: UIEventProperty = {};
  //   Object.entries(events).forEach(([key, fn]) => {
  //     const event = (fn as UIFunctionLike)();
  //     if (isClassConstructor<DecafComponent<Model>>(event)) {
  //       const fn = new event()[key as keyof DecafComponent<Model>] || undefined;
  //       if (fn) {
  //         this.events[key] = fn;
  //         result[key] = fn;
  //       }
  //     } else {
  //       this.events[key] = fn as UIFunctionLike;
  //       result[key] = fn;
  //     }
  //   });
  //   return result;
  // }

  /**
   * @description Retrieves or sets the locale context for the component.
   * @summary Gets the locale identifier from the locale context system. If a locale parameter
   * is provided, it updates the localeRoot property and resolves the new locale context.
   * If no locale is currently set, it initializes it from the localeRoot.
   * @protected
   * @param {string} [locale] - Optional locale identifier to set
   * @return {string} The current locale identifier
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected getLocale(locale?: string): string {
    if (locale || !this.locale) {
      if (locale) this.localeRoot = locale;
      if (this.localeRoot) this.locale = getLocaleContext(this.localeRoot as string);
    }
    return this.locale as string;
  }

  /**
   * @description Retrieves or generates the route path for the component.
   * @summary Gets the navigation route associated with this component. If no route is explicitly
   * set and a model is available, it automatically generates a route based on the model's
   * class name using the pattern `/model/{ModelName}`. Returns an empty string if neither
   * a route nor a model is available.
   * @return {string} The route path for this component
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  getRoute(): string {
    if (!this.route && this.model instanceof Model)
      this.route = `/model/${this.model?.constructor.name}`;
    return this.route || '';
  }

  /**
   * @description Resolves and initializes a model from various input formats.
   * @summary Accepts a model in multiple formats (string name, Model instance, or ModelConstructor)
   * and resolves it to a Model instance. When a string is provided, it looks up the model
   * by name in the Model registry. After resolution, it delegates to setModelDefinitions
   * to complete the model initialization and configuration.
   * @template M - The model type extending from Model
   * @param {string | Model | ModelConstructor<M>} model - The model to resolve and initialize
   * @return {void}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  getModel<M extends Model>(model: string | Model | ModelConstructor<M>): void {
    if (!(model instanceof Model) && typeof model === Primitives.STRING)
      model = Model.get(model as string) as ModelConstructor<M>;
    this.setModelDefinitions(this.model as Model);
  }

  /**
   * @description Configures component properties based on model decorators and metadata.
   * @summary Extracts rendering configuration from the model's decorators using the rendering
   * engine. This includes props, item configuration, and child component definitions. It sets
   * up the mapper for field transformations, configures the item renderer with appropriate
   * properties, and establishes the route for navigation. This method bridges the model's
   * decorator metadata with the component's runtime configuration.
   * @param {Model} model - The model instance to extract definitions from
   * @return {void}
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  setModelDefinitions(model: Model): void {
    if (model instanceof Model) {
      this.getRoute();
      this.model = model;
      const engine = NgxRenderingEngine.get() as unknown as NgxRenderingEngine;
      const field = engine.getDecorators(this.model as Model, {});
      const { props, item, children } = field;
      this.props = Object.assign(props || {}, { children: children || [] }, this.props, {
        handlers: props?.['handlers'] || {},
      });
      if (item?.props?.['mapper']) this.mapper = item?.props!['mapper'] || {};
      this.item = {
        tag: item?.tag || '',
        ...item?.props,
        ...(this.mapper ? { mapper: this.mapper } : {}),
        ...{ route: item?.props?.['route'] || this.route },
      } as KeyValue;
    }
  }

  /**
   * @description Parses and applies properties from the props object to the component instance.
   * @summary This method iterates through the properties of the provided instance object
   * and applies any matching properties from the component's props configuration to the
   * component instance. This allows for dynamic property assignment based on configuration
   * stored in the props object, enabling flexible component customization without requiring
   * explicit property binding for every possible configuration option.
   * The method performs a safe property assignment by checking if each key from the instance
   * exists in the props object before applying it. This prevents accidental property
   * overwriting and ensures only intended properties are modified.
   * @param {KeyValue} instance - The component instance object to process
   * @param {string[]} [skip=[]] - Array of property names to skip during parsing
   * @return {void}
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant D as NgxComponentDirective
   *   participant P as Props Object
   *
   *   C->>D: parseProps(instance, skip)
   *   D->>D: Get Object.keys(instance)
   *   loop For each key in instance
   *     D->>D: Check if key in skip array
   *     alt Key not in skip
   *       D->>P: Check if key exists in this.props
   *       alt Key exists in props
   *         D->>D: Set this[key] = this.props[key]
   *         D->>P: delete this.props[key]
   *       else Key not in props
   *         Note over D: Skip this key
   *       end
   *     end
   *   end
   * @protected
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected parseProps(instance: KeyValue, skip: string[] = [AngularEngineKeys.CHILDREN]): void {
    const props = this.props as KeyValue;
    Object.keys(instance).forEach((key) => {
      if (Object.keys(this.props).includes(key) && !skip.includes(key)) {
        (this as KeyValue)[key] = props[key];
        if (key === BaseComponentProps.HANDLERS) {
          this.parseHandlers(props[key], this as unknown as DecafEventHandler);
        }
        if (key === UIKeys.EVENTS) {
          this.parseEvents(props[key], this);
        }
        delete props[key];
      }
    });
  }

  /**
   * @description Tracks items in ngFor loops for optimal change detection performance.
   * @summary Provides a tracking function for Angular's *ngFor directive to optimize rendering
   * performance. This method generates unique identifiers for list items based on their index
   * and content, allowing Angular to efficiently track changes and minimize DOM manipulations
   * during list updates. The tracking function is essential for maintaining component state
   * and preventing unnecessary re-rendering of unchanged items.
   * @protected
   * @param {number} index - The index of the item in the list
   * @param {KeyValue | string | number} item - The item data to track
   * @return {string | number} A unique identifier for the item
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  protected trackItemFn(index: number, item: KeyValue | string | number): string | number {
    return `${index}-${item}`;
  }

  /**
   * @description Handles custom events from child components or DOM elements.
   * @summary Processes custom events by extracting event handlers and delegating to appropriate
   * handler classes. Supports both CustomEvent format with detail property and direct event
   * objects. If specific handlers are defined in the event, it instantiates the handler class
   * and invokes its handle method. If no handler is found or defined, the event is emitted
   * up the component hierarchy via the listenEvent output.
   * @param {IBaseCustomEvent | ICrudFormEvent | CustomEvent} event - The event to handle
   * @return {Promise<void>} A promise that resolves when event handling is complete
   * @mermaid
   * sequenceDiagram
   *   participant C as Child Component
   *   participant D as NgxComponentDirective
   *   participant H as Event Handler
   *   participant P as Parent Component
   *
   *   C->>D: handleEvent(event)
   *   alt Event is CustomEvent
   *     D->>D: Extract event.detail
   *   end
   *   D->>D: Get event name and handlers
   *   alt Handlers defined
   *     alt Handler exists for event
   *       D->>H: new Handler(router)
   *       D->>H: handle(event)
   *       H-->>D: return result
   *     else No handler found
   *       D->>D: log.debug("No handler found")
   *     end
   *   else No handlers
   *     D->>P: listenEvent.emit(event)
   *   end
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  async handleEvent(event: IBaseCustomEvent & ICrudFormEvent & CustomEvent): Promise<void> {
    let name = '';
    if (event instanceof CustomEvent) {
      if (!event.detail)
        return this.log.for(this.handleEvent).debug(`No details for custom event ${name}`);
      name = event.detail?.name;
      event = event.detail;
    }
    name = name || (event as IBaseCustomEvent)?.['name'];
    const handler = event?.handler || this.handlers[name];
    if (handler) {
      try {
        const { data, role } = event as ICrudFormEvent;
        return await handler.bind(this)(event, data || {}, role);
      } catch (e: unknown) {
        this.log.for(this.handleEvent).error(`Failed to handle ${name} event`, e as Error);
      }
    }
    this.listenEvent.emit(event as IBaseCustomEvent | ICrudFormEvent);
  }

  // passed for ui decorators
  override async submit(...args: unknown[]): Promise<any> {
    this.log.for(this.submit).info(`submit for ${this.componentName} with ${JSON.stringify(args)}`);
  }

  /**
   * @description Determines if a specific operation is allowed in the current context.
   * @summary This method checks if an operation is included in the list of available
   * CRUD operations and if it's not the current operation (unless the current operation
   * is CREATE). This is used to enable/disable or show/hide operation buttons in the UI.
   * Returns false if the operations array is undefined or the operation is not in the list.
   * @param {string} operation - The operation to check
   * @return {boolean} True if the operation is allowed, false otherwise
   * @mermaid
   * sequenceDiagram
   *   participant D as NgxComponentDirective
   *   participant U as UI
   *
   *   U->>D: isAllowed(operation)
   *   alt operations is undefined
   *     D-->>U: Return false
   *   else
   *     D->>D: Check if operation is in operations
   *     D->>D: Check if operation is not current operation
   *     D-->>U: Return result
   *   end
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  isAllowed(operation: string, operations?: CrudOperations[]): boolean {
    if (!operations) {
      operations = this.operations;
    }
    if (operations) {
      return (
        this.operations.includes(operation as CrudOperations) &&
        this.operation !== OperationKeys.CREATE &&
        ((this.operation || '').toLowerCase() !== operation || !this.operation)
      );
    }
    return false;
  }

  /**
   * @description Navigates to a different operation for the current model.
   * @summary This method constructs a navigation URL based on the component's base route,
   * the requested operation, and optionally a model ID. It then uses the Angular router
   * service to navigate to the constructed URL. This is typically used when switching
   * between different CRUD operations (create, read, update, delete) on a model.
   * The URL format is: {route}/{operation}/{id?}
   * @param {string} operation - The operation to navigate to (e.g., 'create', 'read', 'update', 'delete')
   * @param {string} [id] - Optional model ID to include in the navigation URL
   * @return {Promise<boolean>} A promise that resolves to true if navigation was successful
   * @mermaid
   * sequenceDiagram
   *   participant U as UI
   *   participant D as NgxComponentDirective
   *   participant R as Router
   *
   *   U->>D: Click operation button
   *   D->>D: changeOperation(operation, id)
   *   D->>D: Construct navigation URL
   *   Note over D: URL: {route}/{operation}/{id?}
   *   D->>R: navigateByUrl(page)
   *   R->>R: Navigate to new route
   *   R-->>D: Return navigation result
   *   D-->>U: Display new operation view
   * @memberOf module:lib/engine/NgxComponentDirective
   */
  async changeOperation(operation: string, id?: string): Promise<boolean> {
    let page = `${this.route}/${operation}/`.replace('//', '/');
    if (!id) id = this.modelId as string;
    if (this.modelId) page = `${page}${this.modelId || id}`;
    return this.router.navigateByUrl(page);
  }

  async initProps<T extends NgxComponentDirective>(
    props: T | KeyValue,
    map: (keyof T)[] | KeyValue = [],
    instance?: NgxComponentDirective & T,
  ): Promise<void> {
    this._props = signal<T>(props as T);
    if (!instance) instance = this as KeyValue as T;
    if (Array.isArray(map)) {
      map.forEach((key: keyof T) => {
        if (key in instance && instance[key]) (props as T)[key as keyof T] = instance[key];
      });
    }
    Object.entries(props).filter(([key, value]) => {
      if (key in instance || map.includes(key as keyof T)) instance[key as keyof T] = value;
    });

    await this.initialize();
  }
}