Source

lib/engine/NgxParentComponentDirective.ts

/**
 * @module module:lib/engine/NgxParentComponentDirective
 * @description Directive base for parent container components used by the rendering system.
 * @summary Provides NgxParentComponentDirective which offers inputs for children metadata,
 * column/row configuration and parent component wiring used by layout and container components.
 *
 * @link {@link NgxParentComponentDirective}
 */
import { Directive, Input, OnInit } from '@angular/core';
import { NgxComponentDirective } from './NgxComponentDirective';
import { FormParent, KeyValue } from './types';
import {
  FieldDefinition,
  IPagedComponentProperties,
  UIMediaBreakPoints,
  UIMediaBreakPointsType,
  UIModelMetadata,
} from '@decaf-ts/ui-decorators';
import { Model } from '@decaf-ts/decorator-validation';
import { IComponentProperties } from './interfaces';
import { Subscription, timer } from 'rxjs';

/**
 * @description Layout component for creating responsive grid layouts in Angular applications.
 * @summary This component provides a flexible grid system that can be configured with dynamic
 * rows and columns. It supports responsive breakpoints and can render child components within
 * the grid structure. The component extends NgxComponentDirective to inherit common functionality
 * and integrates with the model and component renderer systems.
 *
 * @class NgxParentComponentDirective
 * @extends {NgxComponentDirective}
 * @implements {OnInit}
 */
@Directive()
export class NgxParentComponentDirective extends NgxComponentDirective implements OnInit {
  ngOnInit(): void {
    throw new Error('Method not implemented.');
  }

  /**
   * @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}
   */
  @Input()
  page: number = 1;

  /**
   * @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}
   */
  @Input()
  pages: number | IPagedComponentProperties[] = 1;

  /**
   * @description Array of UI model metadata for the currently active page.
   * @summary Contains only the UI model metadata for fields that should be displayed
   * on the currently active page. This is a filtered subset of the children array,
   * updated whenever the user navigates between pages.
   *
   * @type { UIModelMetadata | UIModelMetadata[] | FieldDefinition | FieldDefinition[] | undefined }
   */
  activePage:
    | UIModelMetadata
    | UIModelMetadata[]
    | FieldDefinition
    | FieldDefinition[]
    | undefined = undefined;

  /**
   * @description The currently active page number.
   * @summary Tracks which page of the multi-step form is currently being displayed.
   * This property is updated as users navigate through the form steps using
   * the next/back buttons or programmatic navigation.
   *
   * @type {number}
   */
  activeIndex: number = 1;

  /**
   * @description The parent form object that represents the parent-child relationship in the form hierarchy.
   * @summary This input binds the parent form object to the directive, enabling hierarchical form structures.
   * It allows the directive to interact with the parent form and manage child components effectively.
   *
   * @type {FormParent}
   */
  @Input()
  parentForm!: FormParent;

  /**
   * @description Array of UI model metadata for all form fields.
   * @summary Contains the complete collection of UI model metadata that defines
   * the structure, validation, and presentation of form fields across all pages.
   * Each metadata object contains information about field type, validation rules,
   * page assignment, and display properties.
   *
   * @type {UIModelMetadata[]}
   */
  @Input()
  children: UIModelMetadata[] | IComponentProperties[] | FieldDefinition[] | KeyValue[] = [];

  /**
   * @description Number of columns or array of column definitions for the grid layout.
   * @summary Defines the column structure of the grid. When a number is provided, it creates
   * that many equal-width columns. When an array is provided, each element can define specific
   * column properties or sizing. This allows for flexible grid layouts that can adapt to
   * different content requirements.
   *
   * @type {(number | string[])}
   * @default 1
   */
  @Input()
  cols: number | string[] = 1;

  /**
   * @description Number of rows or array of row definitions for the grid layout.
   * @summary Defines the row structure of the grid. When a number is provided, it creates
   * that many equal-height rows. When an array is provided, each element can define specific
   * row properties or sizing. This provides control over vertical spacing and content organization.
   *
   * @type {(number | string[])}
   * @default 1
   */
  @Input()
  rows: number | KeyValue[] | string[] = 1;

  /**
   * @description Defines the body style of the card.
   * @summary Specifies the appearance of the card body, allowing customization
   * between default, small, or blank styles. This input is used to control the
   * visual presentation of the card content.
   * @type {'default' | 'small' | 'blank'}
   * @default 'default'
   */
  @Input()
  cardBody: 'default' | 'small' | 'blank' = 'default';

  /**
   * @description Specifies the type of the card.
   * @summary Determines the card's visual style, such as clear or shadowed.
   * This input allows for flexible styling of the card component to match
   * different design requirements.
   * @type {'clear' | 'shadow'}
   * @default 'clear'
   */
  @Input()
  cardType: 'clear' | 'shadow' = 'clear';

  /**
   * @description Media breakpoint for responsive behavior.
   * @summary Determines the responsive breakpoint at which the layout should adapt.
   * This affects how the grid behaves on different screen sizes, allowing for
   * mobile-first or desktop-first responsive design patterns. The breakpoint
   * is automatically processed to ensure compatibility with the UI framework.
   *
   * @type {UIMediaBreakPointsType}
   * @default 'medium'
   */
  @Input()
  breakpoint?: UIMediaBreakPointsType | string = UIMediaBreakPoints.MEDIUM;

  /**
   * @description Determines if the layout should match the parent container's size or configuration.
   * @summary Boolean flag that controls whether the component should adapt its layout to match its parent.
   * When true, the component will attempt to align or size itself according to the parent container.
   *
   * @type {boolean}
   * @default true
   */
  @Input()
  match: boolean = true;

  /**
   * @description Preloads card placeholders for rendering.
   * @summary Used to create an array of placeholder elements for card components,
   * typically to reserve space or trigger rendering logic before actual data is loaded.
   *
   * @type {any[]}
   * @default [undefined]
   */
  preloadCards: string[] = new Array(1);

  /**
   * @description Subscription for timer-based operations.
   * @summary Manages the timer subscription used for asynchronous operations
   * like updating active children after page transitions. This subscription
   * is cleaned up in ngOnDestroy to prevent memory leaks.
   *
   * @private
   * @type {Subscription}
   * @memberOf SteppedFormComponent
   */
  protected timerSubscription!: Subscription;

  override async initialize(model?: Model | string): Promise<void> {
    await super.initialize();
    if (model) {
      this.model = model;
    }
    if (this.model && !this.repository) {
      this._repository = this.repository;
    }
  }

  override async ngOnDestroy(): Promise<void> {
    await super.ngOnDestroy();
    if (this.timerSubscription) this.timerSubscription.unsubscribe();
  }

  protected getActivePage(
    page: number,
    firstClick: boolean = true,
  ): UIModelMetadata | UIModelMetadata[] | FieldDefinition | undefined {
    if (firstClick || this.activeIndex !== page) {
      const content = this.children[page] as FieldDefinition;
      this.activePage = undefined;
      this.preloadCards = [...new Array(1)];
      this.timerSubscription = timer(25).subscribe(
        () => (this.activePage = { ...(this.children[page] as FieldDefinition) }),
      );
      this.activeIndex = page;
      if (content) return content;
      return undefined;
    }
  }
}