Source

lib/engine/NgxBaseComponent.ts

import {
  Input,
  Component,
  Inject,
  ViewChild,
  ElementRef,
  OnChanges,
  SimpleChanges,
  Output,
  EventEmitter,
} from '@angular/core';
import { KeyValue, RendererCustomEvent, StringOrBoolean } from './types';
import {
  generateRandomValue,
  getInjectablesRegistry,
  getLocaleFromClassName,
  getOnWindow,
  isDevelopmentMode,
  stringToBoolean
} from '../helpers/utils';
import { Model } from '@decaf-ts/decorator-validation';
import {
  CrudOperations,
  InternalError,
  OperationKeys
} from '@decaf-ts/db-decorators';
import { BaseComponentProps } from './constants';
import { NgxRenderingEngine } from './NgxRenderingEngine';
import { Logger } from '@decaf-ts/logging';
import { getLogger } from '../for-angular.module';
import { DecafRepository } from '../components/list/constants';
import { Repository } from '@decaf-ts/core';
import { RamAdapter } from '@decaf-ts/core/ram';
import { addIcons } from 'ionicons';

/**
 * @description Base component class that provides common functionality for all Decaf components.
 * @summary The NgxBaseComponent serves as the foundation for all Decaf UI components, providing
 * shared functionality for localization, element references, and styling. This abstract class
 * implements common properties and methods that are used across the component library, ensuring
 * consistent behavior and reducing code duplication. Components that extend this class inherit
 * its capabilities for handling translations, accessing DOM elements, and applying custom styling.
 *
 * @template M - The model type that this component works with
 * @param {string} instance - The component instance token used for identification
 * @param {string} locale - The locale to be used for translations
 * @param {StringOrBoolean} translatable - Whether the component should be translated
 * @param {string} className - Additional CSS classes to apply to the component
 * @param {"ios" | "md" | undefined} mode - Component platform style
 *
 * @component NgxBaseComponent
 * @example
 * ```typescript
 * @Component({
 *   selector: 'app-my-component',
 *   templateUrl: './my-component.component.html',
 *   styleUrls: ['./my-component.component.scss']
 * })
 * export class MyComponent extends NgxBaseComponent {
 *   constructor(@Inject('instanceToken') instance: string) {
 *     super(instance);
 *   }
 *
 *   ngOnInit() {
 *     this.initialize();
 *     // Component-specific initialization
 *   }
 * }
 * ```
 * @mermaid
 * sequenceDiagram
 *   participant App as Application
 *   participant Comp as Component
 *   participant Base as NgxBaseComponent
 *   participant Engine as NgxRenderingEngine
 *
 *   App->>Comp: Create component
 *   Comp->>Base: super(instance)
 *   Base->>Base: Set componentName & componentLocale
 *
 *   App->>Comp: Set @Input properties
 *   Comp->>Base: ngOnChanges(changes)
 *
 *   alt model changed
 *     Base->>Base: getModel(model)
 *     Base->>Engine: getDecorators(model, {})
 *     Engine-->>Base: Return decorator metadata
 *     Base->>Base: Configure mapper and item
 *     Base->>Base: getLocale(translatable)
 *   else locale/translatable changed
 *     Base->>Base: getLocale(translatable)
 *   end
 *
 *   App->>Comp: ngOnInit()
 *   Comp->>Base: initialize()
 *   Base->>Base: Set initialized flag
 */
@Component({
  standalone: true,
  template: '<div><div>',
  host: {'[attr.id]': 'uid'},
})
export abstract class NgxBaseComponent implements OnChanges {
  /**
   * @description Reference to the component's 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 NgxBaseComponent
   */
  @ViewChild('component', { read: ElementRef, static: true })
  component!: ElementRef;

  /**
   * @description The name of the component.
   * @summary Stores the name of the component, which is typically derived from the class name.
   * This property is used internally for various purposes such as logging, deriving the default
   * locale, and potentially for component identification in debugging or error reporting.
   *
   * The `componentName` is set during the component's initialization process and should not
   * be modified externally. It's marked as protected to allow access in derived classes while
   * preventing direct access from outside the component hierarchy.
   *
   * @type {string}
   * @protected
   * @memberOf NgxBaseComponent
   *
   * @example
   * // Inside a derived component class
   * console.log(this.componentName); // Outputs: "MyCustomComponent"
   */
  componentName!: string;

  /**
   * @description Unique identifier for the renderer.
   * @summary A unique identifier used to reference the component's renderer instance.
   * This can be used for targeting specific renderer instances when multiple components
   * are present on the same page.
   *
   * @type {string}
   * @memberOf NgxBaseComponent
   */
  @Input()
  rendererId!: string;

  /**
   * @description Repository model for data operations.
   * @summary The data model repository that this component will use for CRUD operations.
   * This provides a connection to the data layer for retrieving and manipulating data.
   *
   * @type {Model| undefined}
   * @memberOf NgxBaseComponent
   */
  @Input()
  model!:  Model | undefined;

  /**
   * @description The repository for interacting with the data model.
   * @summary Provides a connection to the data layer for retrieving and manipulating data.
   * This is an instance of the `DecafRepository` class from the `@decaf-ts/core` package,
   * which is initialized in the `repository` getter method.
   *
   * The repository is used to perform CRUD (Create, Read, Update, Delete) operations on the
   * data model, such as fetching data, creating new items, updating existing items, and deleting
   * items. It also provides methods for querying and filtering data based on specific criteria.
   *
   * @type {DecafRepository<Model>}
   * @private
   * @memberOf NgxBaseComponent
   */
  protected _repository?: DecafRepository<Model>;

  /**
   * @description Dynamic properties configuration object.
   * @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 NgxBaseComponent
   */
  @Input()
  props: Record<string, unknown> = {};

  /**
   * @description Configuration for list item rendering
   * @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: ""}
   * @memberOf NgxBaseComponent
   */
  @Input()
  item: Record<string, unknown> = { tag: '' };

  /**
   * @description Primary key field name for the 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.
   *
   * @type {string}
   * @default 'id'
   * @memberOf NgxBaseComponent
   */
  @Input()
  pk!: string;

  /**
   * @description Base route for navigation related to this component.
   * @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.
   *
   * @type {string}
   * @memberOf NgxBaseComponent
   */
  @Input()
  route!: string;

  /**
   * @description Available CRUD operations for this component.
   * @summary Defines which CRUD operations (Create, Read, Update, Delete) are available
   * for this component. This affects which operations can be performed on the data.
   *
   * @default [OperationKeys.READ]
   * @memberOf NgxBaseComponent
   */
  @Input()
  operations: CrudOperations[] = [OperationKeys.READ];

  /**
   * @description Unique identifier for the current record.
   * @summary A unique identifier for the current record being displayed or manipulated.
   * This is typically used in conjunction with the primary key for operations on specific records.
   *
   * @type {string | number}
   * @memberOf NgxBaseComponent
   */
  @Input()
  uid!: string | number;

  /**
   * @description Field mapping configuration.
   * @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.
   *
   * @type {Record<string, string>}
   * @memberOf NgxBaseComponent
   */
  @Input()
  mapper: Record<string, string> = {};

  /**
   * @description The locale to be used for translations.
   * @summary Specifies the locale identifier to use when translating component text.
   * This can be set explicitly via input property to override the automatically derived
   * locale from the component name. The locale is typically a language code (e.g., 'en', 'fr')
   * or a language-region code (e.g., 'en-US', 'fr-CA') that determines which translation
   * set to use for the component's text content.
   *
   * @type {string}
   * @memberOf NgxBaseComponent
   */
  @Input()
  locale!: string;

  /**
   * @description Determines if the component should be translated.
   * @summary Controls whether the component's text content should be processed for translation.
   * When true, the component will attempt to translate text using the specified locale.
   * When false, text is displayed as-is without translation. This property accepts either
   * a boolean value or a string that can be converted to a boolean (e.g., 'true', 'false', '1', '0').
   *
   * @type {StringOrBoolean}
   * @default false
   * @memberOf NgxBaseComponent
   */
  @Input()
  translatable: StringOrBoolean = true;

  /**
   * @description Additional CSS class names to apply to the component.
   * @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 NgxBaseComponent
   */
  @Input()
  className: string = '';

  /**
   * @description Component platform style.
   * @summary Controls the visual appearance of the component based on platform design guidelines.
   * The 'ios' mode follows iOS design patterns, while 'md' (Material Design) follows Android/Google
   * design patterns. This property affects various visual aspects such as animations, form elements,
   * and icons. Setting this property allows components to maintain platform-specific styling
   * for a more native look and feel.
   *
   * @type {("ios" | "md" | undefined)}
   * @default "md"
   * @memberOf NgxBaseComponent
   */
  @Input()
  mode: 'ios' | 'md' | undefined = 'md';

  /**
   * @description The locale derived from the component's class name.
   * @summary Stores the automatically derived locale based on the component's class name.
   * This is determined during component initialization and serves as a fallback when no
   * explicit locale is provided via the locale input property. The derivation is handled
   * by the getLocaleFromClassName utility function, which extracts a locale identifier
   * from the component's class name.
   *
   * @type {string}
   * @memberOf NgxBaseComponent
   */
  componentLocale!: string;

  /**
   * @description Controls whether child components should be rendered
   * @summary Determines if child components should be rendered by the component.
   * This can be set to a boolean value or a string that can be converted to a boolean.
   * When true, child components defined in the model will be rendered. When false,
   * child components will be skipped. This provides control over the rendering depth.
   *
   * @type {string | StringOrBoolean}
   * @default true
   * @memberOf NgxBaseComponent
   */
  @Input()
  renderChild: string | StringOrBoolean = true;

  /**
   * @description Flag indicating if the component has been initialized
   * @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
   */
  initialized: boolean = false;

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

  /**
   * @description Reference to the rendering engine instance
   * @summary Provides access to the NgxRenderingEngine singleton instance,
   * which handles the rendering of components based on model definitions.
   * This engine is used to extract decorator metadata and render child components.
   *
   * @type {NgxRenderingEngine}
   */
  renderingEngine: NgxRenderingEngine =
    NgxRenderingEngine.get() as unknown as NgxRenderingEngine;

  /**
   * @description Logger instance for the component.
   * @summary Provides logging capabilities for the component, allowing for consistent
   * and structured logging of information, warnings, and errors. This logger is initialized
   * in the ngOnInit method using the getLogger function from the ForAngularModule.
   *
   * The logger is used throughout the component to record important events, debug information,
   * and potential issues. It helps in monitoring the component's behavior, tracking the flow
   * of operations, and facilitating easier debugging and maintenance.
   *
   * @type {Logger}
   * @private
   * @memberOf NgxBaseComponent
   */
  logger!: Logger;

  /**
   * @description Creates an instance of NgxBaseComponent.
   * @summary Initializes a new instance of the base component with the provided instance token.
   * This constructor sets up the fundamental properties required by all Decaf components,
   * including the component name, locale settings, and logging capabilities. The instance
   * token is used for component identification and locale derivation.
   *
   * The constructor performs the following initialization steps:
   * 1. Sets the componentName from the provided instance token
   * 2. Derives the componentLocale from the class name using utility functions
   * 3. Initializes the logger instance for the component
   *
   * @param {string} instance - The component instance token used for identification
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular
   *   participant C as Component
   *   participant B as NgxBaseComponent
   *   participant U as Utils
   *   participant L as Logger
   *
   *   A->>C: new Component(instance)
   *   C->>B: super(instance)
   *   B->>B: Set componentName = instance
   *   B->>U: getLocaleFromClassName(instance)
   *   U-->>B: Return derived locale
   *   B->>B: Set componentLocale
   *   B->>L: getLogger(this)
   *   L-->>B: Return logger instance
   *   B->>B: Set logger
   *
   * @memberOf NgxBaseComponent
   */
  // eslint-disable-next-line @angular-eslint/prefer-inject
  protected constructor(@Inject('instanceToken') protected instance: string, @Inject('iconsToken') icons: KeyValue = {}) {
    if(Object.keys(icons).length)
      addIcons(icons);
    this.componentName = instance;
    this.componentLocale = getLocaleFromClassName(instance);
    this.logger = getLogger(this);
    this.getLocale(this.translatable);
    this.uid = generateRandomValue(12);
  }

  /**
   * @description Getter for the repository instance.
   * @summary Provides a connection to the data layer for retrieving and manipulating data.
   * This method initializes the `_repository` property if it is not already set, ensuring
   * that a single instance of the repository is used throughout the component.
   *
   * The repository is used to perform CRUD operations on the data model, such as fetching data,
   * creating new items, updating existing items, and deleting items. It also provides methods
   * for querying and filtering data based on specific criteria.
   *
   * @returns {DecafRepository<Model>} The initialized repository instance.
   * @private
   * @memberOf NgxBaseComponent
   */
  protected get repository(): DecafRepository<Model> {
    try {
      if (!this._repository) {
        const modelName  = (this.model as Model).constructor.name
        const constructor = Model.get(modelName);
        if (!constructor)
          throw new InternalError(
            'Cannot find model. was it registered with @model?',
          );
        let dbAdapterFlavour = getOnWindow('dbAdapterFlavour');
        if(!dbAdapterFlavour && isDevelopmentMode()) {
          const adapter = new RamAdapter();
          dbAdapterFlavour = adapter.flavour;
        }
        this._repository = Repository.forModel(constructor, dbAdapterFlavour as string);
        this.model = new constructor() as Model;
        if(this.model && !this.pk)
          this.pk = (this._repository as unknown as DecafRepository<Model>).pk || 'id';
      }
    } catch (error: unknown) {
      throw new InternalError(
        (error as Error)?.message || error as string
      );
    }
    return this._repository;
  }

  /**
   * @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
   * @return {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant B as NgxBaseComponent
   *   participant P as Props Object
   *
   *   C->>B: parseProps(instance)
   *   B->>B: Get Object.keys(instance)
   *   loop For each key in instance
   *     B->>P: Check if key exists in this.props
   *     alt Key exists in props
   *       B->>B: Set this[key] = this.props[key]
   *     else Key not in props
   *       Note over B: Skip this key
   *     end
   *   end
   *
   * @protected
   * @memberOf NgxBaseComponent
   */
  protected parseProps(instance: KeyValue): void {
    Object.keys(instance).forEach((key) => {
      if(Object.keys(this.props).includes(key))
        (this as KeyValue)[key] = this.props[key];
    })
  }

  /**
   * @description Handles changes to component inputs
   * @summary This Angular lifecycle hook is called when input properties change.
   * It responds to changes in the model, locale, or translatable properties by
   * updating the component's internal state accordingly. When the model changes,
   * it calls getModel to process the new model and getLocale to update the locale.
   * When locale or translatable properties change, it calls getLocale to update
   * the translation settings.
   *
   * @param {SimpleChanges} changes - Object containing changed properties
   * @return {void}
   */
  ngOnChanges(changes: SimpleChanges): void {
    if (changes[BaseComponentProps.MODEL]) {
      const { currentValue } = changes[BaseComponentProps.MODEL];
      if (currentValue) this.getModel(currentValue);
      this.getLocale(this.translatable);
    }
    if (
      changes[BaseComponentProps.INITIALIZED] ||
      changes[BaseComponentProps.LOCALE] ||
      changes[BaseComponentProps.TRANSLATABLE]
    )
      this.getLocale(this.translatable);
  }

  /**
   * @description Gets the appropriate locale string based on the translatable flag and available locales.
   * @summary Determines which locale string to use for translation based on the translatable flag
   * and available locale settings. This method first converts the translatable parameter to a boolean
   * using the stringToBoolean utility function. If translatable is false, it returns an empty string,
   * indicating no translation should be performed. If translatable is true, it checks for an explicitly
   * provided locale via the locale property. If no explicit locale is available, it falls back to the
   * componentLocale derived from the component's class name.
   *
   * @param {StringOrBoolean} translatable - Whether the component should be translated
   * @return {string} The locale string to use for translation, or empty string if not translatable
   *
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant N as NgxBaseComponent
   *   participant S as StringUtils
   *
   *   C->>N: getLocale(translatable)
   *   N->>S: stringToBoolean(translatable)
   *   S-->>N: Return boolean value
   *   N->>N: Store in this.translatable
   *   alt translatable is false
   *     N-->>C: Return empty string
   *   else translatable is true
   *     alt this.locale is defined
   *       N-->>C: Return this.locale
   *     else this.locale is not defined
   *       N-->>C: Return this.componentLocale
   *     end
   *   end
   */
  getLocale(translatable: StringOrBoolean): string {
    this.translatable = stringToBoolean(translatable);
    if (!this.translatable)
      return '';
    if (!this.locale)
      this.locale = this.componentLocale;
    return this.locale;
  }

  /**
   * @description Gets the route for the component
   * @summary Retrieves the route path for the component, generating one based on the model
   * if no route is explicitly set. This method checks if a route is already defined, and if not,
   * it creates a default route based on the model's constructor name. The generated route follows
   * the pattern '/model/{ModelName}'. This is useful for automatic routing in CRUD operations.
   *
   * @return {string} The route path for the component, or empty string if no route is available
   */
  getRoute(): string {
    if (!this.route && this.model instanceof Model)
      this.route = `/model/${this.model?.constructor.name}`;
    return this.route || '';
  }

  /**
   * @description Resolves and sets the component's model
   * @summary Processes the provided model parameter, which can be either a Model instance
   * or a string identifier. If a string is provided, it attempts to resolve the actual model
   * from the injectables registry. After resolving the model, it calls setModelDefinitions
   * to configure the component based on the model's metadata.
   *
   * @param {string | Model} model - The model instance or identifier string
   * @return {void}
   */
  getModel(model: string | Model): void {
    if (!(model instanceof Model))
      this.model = getInjectablesRegistry().get(model) as Model;
    this.setModelDefinitions(this.model as Model);
  }

  /**
   * @description Configures component properties based on model metadata
   * @summary Extracts and applies configuration from the model's decorators to set up
   * the component. This method uses the rendering engine to retrieve decorator metadata
   * from the model, then configures the component's mapper and item properties accordingly.
   * It ensures the route is properly set and merges various properties from the model's
   * metadata into the component's configuration.
   *
   * @param {Model} model - The model to extract configuration from
   * @return {void}
   */
  setModelDefinitions(model: Model): void {
    if (model instanceof Model) {
      this.getRoute();
      this.model = model;
      const field = this.renderingEngine.getDecorators(this.model as Model, {});
      const{ props, item, children } = field;
      this.props = Object.assign(props || {}, {children: children || []});
      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 },
      };
    }
  }

  /**
   * @description Initializes the component
   * @summary Performs one-time initialization of the component. This method checks if
   * the component has already been initialized to prevent duplicate initialization.
   * When called for the first time, it sets the initialized flag to true and logs
   * an initialization message with the component name. This method is typically called
   * during the component's lifecycle setup.
   */
  initialize(): void {
    this.initialized = true;
  }

  /**
   * @description Handles custom events from child components.
   * @summary Receives events from child renderer components and forwards them to parent
   * components through the listenEvent output. This creates an event propagation chain
   * that allows events to bubble up through the component hierarchy, enabling coordinated
   * responses to user interactions across the layout structure.
   *
   * @param {RendererCustomEvent} event - The custom event from a child component
   * @return {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant C as Child Component
   *   participant L as NgxBaseComponent
   *   participant P as Parent Component
   *
   *   C->>L: Emit RendererCustomEvent
   *   L->>L: handleEvent(event)
   *   L->>P: listenEvent.emit(event)
   *   Note over P: Handle event in parent
   *
   * @memberOf NgxBaseComponent
   */
  handleEvent(event: RendererCustomEvent): void {
    this.listenEvent.emit(event);
  }

  /**
   * @description Tracks items in ngFor loops for optimal change detection.
   * @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.
   *
   * @param {number} index - The index of the item in the list
   * @param {KeyValue | string | number} item - The item data to track
   * @returns {string | number} A unique identifier for the item
   * @memberOf NgxBaseComponent
   */
  trackItemFn(index: number, item: KeyValue | string | number): string | number {
    return `${index}-${item}`;
  }
}