Source

lib/components/stepped-form/stepped-form.component.ts

/**
 * @module module:lib/components/stepped-form/stepped-form.component
 * @description Stepped form component module.
 * @summary Provides `SteppedFormComponent` which implements a multi-page form
 * UI with navigation, validation and submission support. Useful for forms that
 * need to be split into logical steps/pages.
 *
 * @link {@link SteppedFormComponent}
 */

import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { TranslatePipe } from '@ngx-translate/core';
import { IonButton, IonSkeletonText, IonText } from '@ionic/angular/standalone';
import { arrowForwardOutline, arrowBackOutline } from 'ionicons/icons';
import { addIcons } from 'ionicons';
import { UIElementMetadata, UIModelMetadata} from '@decaf-ts/ui-decorators';
import { CrudOperations, OperationKeys } from '@decaf-ts/db-decorators';
import { ComponentEventNames } from '../../engine/constants';
import { Dynamic } from '../../engine/decorators';
import { NgxFormService } from '../../services/NgxFormService';
import { getLocaleContext } from '../../i18n/Loader';
import { LayoutComponent } from '../layout/layout.component';
import { NgxFormDirective } from '../../engine/NgxFormDirective';
import { FormParent } from '../../engine/types';


@Dynamic()
@Component({
  selector: 'ngx-decaf-stepped-form',
  templateUrl: './stepped-form.component.html',
  styleUrls: ['./stepped-form.component.scss'],
  imports: [
    TranslatePipe,
    ReactiveFormsModule,
    IonSkeletonText,
    IonText,
    IonButton,
    LayoutComponent
  ],
  standalone: true,
   host: {'[attr.id]': 'uid'},
})
export class SteppedFormComponent extends NgxFormDirective implements OnInit, OnDestroy {

  /**
   * @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[]}
   * @memberOf SteppedFormComponent
   */
  @Input()
  override children!: UIModelMetadata[] | { title: string; description: string; items?:  UIModelMetadata[]  }[];


  /**
   * @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 SteppedFormComponent
   */
  @Input()
  paginated: boolean = true;


  // /**
  //  * @description Optional action identifier for form submission context.
  //  * @summary Specifies a custom action name that will be included in the submit event.
  //  * If not provided, defaults to the standard submit event constant. Used to
  //  * distinguish between different types of form submissions within the same component.
  //  *
  //  * @type {string | undefined}
  //  */
  // @Input()
  // action?: string;



  /**
   * @description Number of pages in the stepped form.
   * @summary Represents the total number of steps/pages in the multi-step form.
   * This value is automatically calculated based on the page properties of the children
   * or can be explicitly set. Each page represents a logical group of form fields.
   *
   * @type {number}
   * @default 1
   * @memberOf SteppedFormComponent
   */
  @Input()
  override pages: number | {title: string; description: string}[] = 1;

  /**
   * List of titles and descriptions for each page of the stepped form.
   * Each object in the array represents a page, containing a title and a description.
   *
   * @example
   * pageTitles = [
   *   { title: 'Personal Information', description: 'Fill in your personal details.' },
   *   { title: 'Address', description: 'Provide your residential address.' }
   * ];
   */
  @Input()
  pageTitles: { title: string; description: string}[] = [];


  /**
   * @description The CRUD operation type for this form.
   * @summary Defines the type of operation being performed (CREATE, READ, UPDATE, DELETE).
   * This affects form behavior, validation rules, and field accessibility. For example,
   * READ operations might disable form fields, while CREATE operations enable all fields.
   *
   * @type {CrudOperations}
   * @default OperationKeys.CREATE
   * @memberOf SteppedFormComponent
   */
  @Input()
  override operation: CrudOperations = OperationKeys.CREATE;

  /**
   * @description The initial page to display when the form loads.
   * @summary Specifies which page of the multi-step form should be shown first.
   * This allows starting the form at any step, useful for scenarios like editing
   * existing data where you might want to jump to a specific section.
   *
   * @type {number}
   * @default 1
   * @memberOf SteppedFormComponent
   */
  @Input()
  startPage: number = 1;

  // /**
  //  * @description Angular reactive FormGroup or FormArray for form state management.
  //  * @summary The form instance that manages all form controls, validation, and form state.
  //  * When using FormArray, each array element represents a page's FormGroup. When using
  //  * FormGroup, it contains all form controls for the entire stepped form.
  //  *
  //  * @type {FormGroup | FormArray | undefined}
  //  * @memberOf SteppedFormComponent
  //  */
  // @Input()
  // formGroup!: FormGroup | FormArray | undefined;

  /**
   * @description Array representing the structure of pages.
   * @summary Contains metadata about each page, including page numbers and indices.
   * This array is built during initialization to organize the form fields into
   * logical pages and provide navigation structure.
   *
   * @type {UIModelMetadata[]}
   * @memberOf SteppedFormComponent
   */
  pagesArray: UIModelMetadata[] = [];


  // /**
  //  * @description Custom event handlers for form actions.
  //  * @summary A record of event handler functions keyed by event names that can be
  //  * triggered during form operations. These handlers provide extensibility for
  //  * custom business logic and can be invoked for various form events and actions.
  //  *
  //  * @type {HandlerLike}
  //  */
  // @Input()
  // handlers!: HandlerLike;



  /**
   * @description Event emitter for form submission.
   * @summary Emits events when the form is submitted, typically on the last page
   * when all validation passes. The emitted event contains the form data and
   * event type information for parent components to handle.
   *
   * @type {EventEmitter<IBaseCustomEvent>}
   * @memberOf SteppedFormComponent
   */
  // @Output()
  // submitEvent: EventEmitter<ICrudFormEvent> = new EventEmitter<ICrudFormEvent>();

  /**
   * @description Creates an instance of SteppedFormComponent.
   * @summary Initializes a new SteppedFormComponent instance and registers the required
   * Ionic icons for navigation buttons (forward and back arrows).
   *
   * @memberOf SteppedFormComponent
   */
  constructor() {
    super("SteppedFormComponent");
    addIcons({arrowForwardOutline, arrowBackOutline});
    this.enableDarkMode = true;
  }

  /**
   * @description Initializes the component after Angular first displays the data-bound properties.
   * @summary Sets up the stepped form by organizing children into pages, calculating the total
   * number of pages, and initializing the active page. This method processes the UI model metadata
   * to create a logical page structure and ensures proper page assignments for all form fields.
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular Lifecycle
   *   participant S as SteppedFormComponent
   *   participant F as Form Service
   *
   *   A->>S: ngOnInit()
   *   S->>S: Set activeIndex = startPage
   *   S->>S: Calculate total pages
   *   S->>S: Assign page props to children
   *   S->>S: getCurrentFormGroup(activeIndex)
   *   S->>F: Extract FormGroup for active page
   *   F-->>S: Return activeFormGroup
   *
   * @memberOf SteppedFormComponent
   */
  override async ngOnInit(): Promise<void>  {
    if (!this.locale)
      this.locale = getLocaleContext("SteppedFormComponent")
    this.activeIndex = this.startPage;
    if (typeof this.pages === 'object') {
      this.pageTitles = this.pages;
    } else {
       if (!this.pageTitles.length)
      this.pageTitles =  Array.from({ length: this.pages }, () => ({ title: '', description: ''}));
    }

    this.pages = this.pageTitles.length;

    if (this.paginated) {
      if (!this.parentForm)
        this.parentForm = (this.formGroup?.root || this.formGroup) as FormParent;
      this.children = [... (this.children as UIModelMetadata[]).map((c) => {
        if (!c.props)
          c.props = {};
        const page = c.props['page'] || 1;
        // prevent page overflow
        c.props['page'] = page > this.pages ? this.pages : page;
        return c;
      })];
      this.getActivePage(this.activeIndex);
    } else {
      this.children =  this.pageTitles.map((page, index) => {
        const pageNumber = index + 1;
        const items = (this.children as UIModelMetadata[]).filter(({ props }: UIElementMetadata) => props?.['page'] === pageNumber);
        return {
          page: pageNumber,
          title: page.title,
          description: page.description,
          items
        };
      });
      // this.formGroup =  new FormGroup(
      //   (this.formGroup as FormArray).controls.reduce((acc, control, index) => {
      //     acc[index] = control as FormGroup;
      //     return acc;
      //   }, {} as Record<string, FormGroup>)
      // );
    }
    this.initialized = true;
  }

  /**
   * @description Cleanup method called when the component is destroyed.
   * @summary Unsubscribes from any active timer subscriptions to prevent memory leaks.
   * This is part of Angular's component lifecycle and ensures proper resource cleanup.
   *
   * @memberOf SteppedFormComponent
   */
  override ngOnDestroy(): void {
    super.ngOnDestroy();
    if (this.timerSubscription)
      this.timerSubscription.unsubscribe();
  }

  /**
   * @description Handles navigation to the next page or form submission.
   * @summary Validates the current page's form fields and either navigates to the next page
   * or submits the entire form if on the last page. Form validation must pass before
   * proceeding. On successful submission, emits a submit event with form data.
   *
   * @param {boolean} lastPage - Whether this is the last page of the form
   * @return {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant S as SteppedFormComponent
   *   participant F as Form Service
   *   participant P as Parent Component
   *
   *   U->>S: Click Next/Submit
   *   S->>F: validateFields(activeFormGroup)
   *   F-->>S: Return validation result
   *   alt Not last page and valid
   *     S->>S: activeIndex++
   *     S->>S: getCurrentFormGroup(activeIndex)
   *   else Last page and valid
   *     S->>F: getFormData(formGroup)
   *     F-->>S: Return form data
   *     S->>P: submitEvent.emit({data, name: SUBMIT})
   *   end
   *
   * @memberOf SteppedFormComponent
   */
  handleNext(lastPage: boolean = false): void {
    const isValid = NgxFormService.validateFields(this.formGroup as FormGroup);
    // const isValid = this.paginated ?
    //   NgxFormService.validateFields(this.formGroup as FormGroup) :
    //   (this.formGroup as FormArray)?.controls.every(control => NgxFormService.validateFields(control as FormGroup));
    if (!lastPage) {
      if (isValid) {
        this.activeIndex = this.activeIndex + 1;
        this.getActivePage(this.activeIndex);
      }
    } else {
     if (isValid) {
      const rootForm = this.formGroup?.root || this.formGroup;
      const data = Object.assign({}, ...Object.values(NgxFormService.getFormData(rootForm as FormGroup)));
      this.submitEvent.emit({
        data,
        component:  this.componentName,
        name: this.action || ComponentEventNames.SUBMIT,
        handlers: this.handlers,
      });
     }
    }
  }

  /**
   * @description Handles navigation to the previous page.
   * @summary Moves the user back to the previous page in the stepped form.
   * This method decrements the active page number and updates the form
   * group and children to display the previous page's content.
   *
   * @return {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant S as SteppedFormComponent
   *
   *   U->>S: Click Back
   *   S->>S: activeIndex--
   *   S->>S: getCurrentFormGroup(activeIndex)
   *   Note over S: Update active form and children
   *
   * @memberOf SteppedFormComponent
   */
  handleBack(): void {
    if (!this.paginated)
      return this.location.back();
    this.activeIndex = this.activeIndex - 1;
    this.getActivePage(this.activeIndex);
  }



  // async submit(event?: SubmitEvent, eventName?: string, componentName?: string): Promise<boolean | void> {
  //   if (event) {
  //     event.preventDefault();
  //     event.stopImmediatePropagation();
  //   }

  //   if (!NgxFormService.validateFields(this.formGroup as FormGroup))
  //     return false;
  //   const data = NgxFormService.getFormData(this.formGroup as FormGroup);
  //   this.submitEvent.emit({
  //     data,
  //     component: componentName || this.componentName,
  //     name: eventName || this.action || ComponentEventNames.SUBMIT,
  //     handlers: this.handlers,
  //   });
  // }
}