Source

lib/services/NgxFormService.ts

/**
 * @module lib/engine/NgxFormService
 * @description Utilities to create and manage Angular forms in Decaf components.
 * @summary The NgxFormService exposes helpers to build FormGroup/FormArray instances
 * from component metadata or UI model definitions, register forms in a registry,
 * validate and extract form data, and create controls with appropriate validators.
 */
import { escapeHtml, FieldProperties, HTML5CheckTypes, HTML5InputTypes, IPagedComponentProperties, parseToNumber, UIModelMetadata } from '@decaf-ts/ui-decorators';
import { FieldUpdateMode, FormParent, FormParentGroup, KeyValue } from '../engine/types';
import { IComponentConfig, IFormComponentProperties } from '../engine/interfaces';
import { AbstractControl, FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { isValidDate, ModelKeys, parseDate, Primitives, Validation } from '@decaf-ts/decorator-validation';
import { ValidatorFactory } from '../engine/ValidatorFactory';
import { cleanSpaces } from '../utils/helpers';
import { OperationKeys } from '@decaf-ts/db-decorators';
import { BaseComponentProps } from '../engine/constants';


/**
 * @description Service for managing Angular forms and form controls.
 * @summary The NgxFormService provides utility methods for creating, managing, and validating Angular forms and form controls. It includes functionality for registering forms, adding controls, validating fields, and handling form data.
 *
 * @class
 * @param {WeakMap<AbstractControl, FieldProperties>} controls - A WeakMap to store control properties.
 * @param {Map<string, FormGroup>} formRegistry - A Map to store registered forms.
 *
 * @example
 * // Creating a form from components
 * const components = [
 *   { inputs: { name: 'username', type: 'text', required: true } },
 *   { inputs: { name: 'password', type: 'password', minLength: 8 } }
 * ];
 * const form = NgxFormService.createFormFromComponents('loginForm', components, true);
 *
 * // Validating fields
 * NgxFormService.validateFields(form);
 *
 * // Getting form data
 * const formData = NgxFormService.getFormData(form);
 * @mermaid
 * sequenceDiagram
 *   participant C as Component
 *   participant NFS as NgxFormService
 *   participant AF as Angular Forms
 *   C->>NFS: createFormFromComponents()
 *   NFS->>AF: new FormGroup()
 *   NFS->>NFS: addFormControl()
 *   NFS->>AF: addControl()
 *   NFS-->>C: Return FormGroup
 *   C->>NFS: validateFields()
 *   NFS->>AF: markAsTouched(), markAsDirty(), updateValueAndValidity()
 *   C->>NFS: getFormData()
 *   NFS->>AF: Get control values
 *   NFS-->>C: Return form data
 */
export class NgxFormService {
  /**
   * @description WeakMap that stores control properties for form controls.
   * @summary A WeakMap that associates AbstractControl instances with their corresponding FieldProperties.
   * This allows the service to track metadata for form controls without creating memory leaks.
   * @type {WeakMap<AbstractControl, FieldProperties>}
   * @private
   * @static
   */
  private static controls: WeakMap<AbstractControl, FieldProperties> = new WeakMap<AbstractControl, FieldProperties>();

  /**
   * @description Registry of form groups indexed by their unique identifiers.
   * @summary A Map that stores FormGroup instances with their unique string identifiers.
   * This allows global access to registered forms throughout the application.
   * @type {Map<string, FormGroup>}
   * @private
   * @static
   */
  private static formRegistry: Map<string, FormParent> = new Map<string, FormParent>();

  /**
   * @description Creates a new form group or form array with the specified identifier.
   * @summary Generates a FormGroup or FormArray based on the provided parameters. If formArray is true,
   * creates a FormArray; otherwise creates a FormGroup. The form can optionally be registered in the
   * global form registry for later access throughout the application. If a form with the given id
   * already exists in the registry, it returns the existing form.
   * @param {string} id - Unique identifier for the form
   * @param {boolean} [formArray=false] - Whether to create a FormArray instead of a FormGroup
   * @param {boolean} [registry=true] - Whether to register the form in the global registry
   * @return {FormGroup | FormArray} The created or existing form instance
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant FR as Form Registry
   *   participant AF as Angular Forms
   *   C->>NFS: createForm(id, formArray, registry)
   *   NFS->>FR: Check if form exists
   *   alt Form exists
   *     FR-->>NFS: Return existing form
   *   else Form doesn't exist
   *     alt formArray is true
   *       NFS->>AF: new FormArray([])
   *     else
   *       NFS->>AF: new FormGroup({})
   *     end
   *     alt registry is true
   *       NFS->>FR: addRegistry(id, form)
   *     end
   *   end
   *   NFS-->>C: Return FormGroup | FormArray
   * @static
   */
  static createForm(id: string, formArray = false, registry: boolean = true): FormGroup | FormArray {
    const form = this.formRegistry.get(id) ?? (formArray ? new FormArray([]) : new FormGroup({}));
    if (!this.formRegistry.has(id) && registry)
      this.addRegistry(id, form as FormArray | FormGroup);
    return form as FormArray | FormGroup;
  }


  /**
   * @description Adds a form to the registry.
   * @summary Registers a FormGroup or FormArray with a unique identifier for global access throughout
   * the application. This allows forms to be retrieved and managed centrally. Throws an error if
   * the identifier is already in use to prevent conflicts.
   * @param {string} formId - The unique identifier for the form
   * @param {FormParent} formGroup - The FormGroup or FormArray to be registered
   * @return {void}
   * @throws {Error} If a FormGroup with the given id is already registered
   * @static
   */
  static addRegistry(formId: string, formGroup: FormParent): void {
    if (this.formRegistry.has(formId))
      throw new Error(`A FormGroup with id '${formId}' is already registered.`);
    this.formRegistry.set(formId, formGroup);
  }

  /**
   * @description Retrieves a form from the registry by its identifier.
   * @summary Gets a FormGroup or FormArray from the registry using its unique identifier.
   * Returns undefined if the form is not found in the registry. This method provides
   * safe access to registered forms without throwing errors.
   * @param {string} [id] - The unique identifier of the form to retrieve
   * @return {FormParent | undefined} The FormGroup or FormArray if found, undefined otherwise
   * @static
   */
  static getOnRegistry(id?: string): FormParent | undefined {
    return this.formRegistry.get(id as string);
  }

  /**
   * @description Removes a form from the registry.
   * @summary Deletes a FormGroup or FormArray from the registry using its unique identifier.
   * This cleans up the registry and allows the identifier to be reused. The form itself
   * is not destroyed, only removed from the central registry.
   * @param {string} formId - The unique identifier of the form to be removed
   * @return {void}
   * @static
   */
  static removeRegistry(formId: string): void {
    this.formRegistry.delete(formId);
  }

  /**
   * @description Resolves the parent group and control name from a path.
   * @summary Traverses the form group structure to find the parent group and control name for a given path.
   * Handles complex nested structures including arrays and sub-groups. Creates missing intermediate
   * groups as needed and properly configures FormArray controls for multiple value scenarios.
   * @param {FormGroup} formGroup - The root FormGroup to traverse
   * @param {string} path - The dot-separated path to the control (e.g., 'user.address.street')
   * @param {IFormComponentProperties} componentProps - Properties defining the component configuration
   * @param {KeyValue} parentProps - Properties from the parent component for context
   * @return {FormParentGroup} A tuple containing the parent FormGroup and the control name
   * @private
   * @mermaid
   * sequenceDiagram
   *   participant NFS as NgxFormService
   *   participant FG as FormGroup
   *   participant FA as FormArray
   *   NFS->>NFS: Split path into parts
   *   loop For each path part
   *     alt Control doesn't exist
   *       alt isMultiple and part is childOf
   *         NFS->>FA: new FormArray([new FormGroup({})])
   *       else
   *         NFS->>FG: new FormGroup({})
   *       end
   *       NFS->>FG: addControl(part, newControl)
   *     end
   *     NFS->>NFS: Navigate to next level
   *   end
   *   NFS-->>NFS: Return [parentGroup, controlName]
   * @static
   */
  private static resolveParentGroup(
    formGroup: FormGroup,
    path: string,
    componentProps: IFormComponentProperties,
    parentProps: Partial<IFormComponentProperties>
  ): FormParentGroup {
    const isMultiple = parentProps?.multiple || parentProps?.type === Array.name || false;
    const parts = path.split('.');
    const controlName = parts.pop() as string;

    const {childOf} = componentProps
    let currentGroup = formGroup;
    function setArrayComponentProps(formGroupArray: KeyValue) {
      const props = formGroupArray?.[BaseComponentProps.FORM_GROUP_COMPONENT_PROPS] || {};
        if(!props[ModelKeys.MODEL][controlName])
          props[ModelKeys.MODEL] = Object.assign({}, props[ModelKeys.MODEL], {[controlName]: {...componentProps}});
    }

    for (const part of parts) {
      if (!currentGroup.get(part)) {
        const partFormGroup = (isMultiple && (part === childOf|| childOf?.endsWith(`${part}`))) ? new FormArray([new FormGroup({})]) : new FormGroup({});
        const pk = componentProps?.pk || parentProps?.pk || '';
        (partFormGroup as KeyValue)[BaseComponentProps.FORM_GROUP_COMPONENT_PROPS] = {
          childOf: childOf || '',
          isMultiple: isMultiple,
          required: parentProps?.required ?? false,
          name: part,
          pk,
          [ModelKeys.MODEL]: {},
        } as Partial<FieldProperties> & {model: KeyValue};

        if(currentGroup instanceof FormArray) {
          (currentGroup as FormArray).push(partFormGroup);
        } else {
          for(const control of Object.values(partFormGroup.controls)) {
            if(control instanceof FormControl)
              this.register(control as AbstractControl, componentProps);
          }

          if(partFormGroup instanceof AbstractControl)
            this.register(partFormGroup as AbstractControl, componentProps);
          currentGroup.addControl(part, partFormGroup);
        }
      }
      if(childOf && currentGroup instanceof FormArray)
        setArrayComponentProps(currentGroup);
      currentGroup = currentGroup.get(part) as FormGroup;
    }
    return [currentGroup, controlName];
  }

  /**
   * @description Retrieves component properties from a FormGroup or FormArray.
   * @summary Extracts component properties stored in the form group metadata. If a FormGroup is provided
   * and groupArrayName is specified, it will look for the FormArray within the form structure.
   * @param {FormGroup | FormArray} formGroup - The form group or form array to extract properties from
   * @param {string} [key] - Optional key to retrieve a specific property
   * @param {string} [groupArrayName] - Optional name of the group array if formGroup is not a FormArray
   * @return {Partial<FieldProperties>} The component properties or a specific property if key is provided
   * @static
   */
  static getComponentPropsFromGroupArray(formGroup: FormGroup | FormArray, key?: string, groupArrayName?: string | undefined): Partial<FieldProperties> {
    if(!(formGroup instanceof FormArray) && typeof groupArrayName === Primitives.STRING)
      formGroup = formGroup.root.get(groupArrayName as string) as FormArray || {};
    const props = (formGroup as KeyValue)?.[BaseComponentProps.FORM_GROUP_COMPONENT_PROPS] || {};
    return (!key ? props : props?.[key]) || {};
  }

  /**
   * @description Adds a new group to a parent FormArray.
   * @summary Creates and adds a new FormGroup to the specified parent FormArray based on the
   * component properties stored in the parent's metadata. This is used for dynamic form arrays
   * where new groups need to be added at runtime. Clones the control at the specified index
   * to maintain the same structure and validators.
   * @param {FormParent} parentForm - The FormArray or FormGroup containing the parent FormArray
   * @param {number} [index] - The index position to clone from; defaults to last index if length > 0, otherwise 0
   * @return {FormArray} The parent FormArray after adding the new group
   * @static
   */
  static addGroupToParent(parentForm: FormParent, index?: number): FormArray {
    if(parentForm instanceof FormGroup)
      parentForm = parentForm.parent as FormArray;
    index = index || (parentForm.length === 0 ? 0 : parentForm.length - 1);
    parentForm.push(this.cloneFormControl(parentForm.at(index)));
    return parentForm;
  }

  /**
   * @description Retrieves a FormGroup from a parent FormArray at the specified index.
   * @summary Gets a FormGroup from the specified parent FormArray. If the group doesn't exist
   * at the given index, it will create a new one using addGroupToParent.
   * @param {FormParent} formGroup - The root form group containing the parent FormArray
   * @param {string} parentName - The name of the parent FormArray to retrieve the group from
   * @param {number} [index=1] - The index of the group to retrieve
   * @return {FormGroup} The FormGroup at the specified index
   * @static
   */
  static getGroupFromParent(formGroup: FormParent, parentName: string, index: number = 1): FormGroup {
    const childGroup = ((formGroup.get(parentName) || formGroup) as FormArray).at(index);
    if(childGroup instanceof FormGroup)
      return childGroup;
    return this.addGroupToParent(formGroup, index).at(index) as FormGroup;
  }

  /**
   * @description Clones a form control with its validators.
   * @summary Creates a deep copy of a FormControl, FormGroup, or FormArray, preserving
   * validators but resetting values and state. This is useful for creating new instances
   * of form controls with the same validation rules, particularly in dynamic FormArrays
   * where new groups need to be added with identical structure.
   * @param {AbstractControl} control - The control to clone (FormControl, FormGroup, or FormArray)
   * @return {AbstractControl} A new instance of the control with the same validators
   * @throws {Error} If the control type is not supported
   * @static
   */
  static cloneFormControl(control: AbstractControl): AbstractControl {
    const syncValidators = (control.validator ? [control.validator] : []).filter(fn => {
      // if(lastIndex > 0)
      //   if(fn !== Validators.required)
      //     return fn;
      return fn;
    });
    const asyncValidators = control.asyncValidator ?? null;
    const validators = {
       validators: syncValidators,
       asyncValidators
    }
    if (control instanceof FormControl) {
      control = new FormControl("", validators);
      // control.markAsPristine();
      // control.markAsUntouched();
      // control.setErrors(null);
      // control.updateValueAndValidity();
      return control;
    }

    if (control instanceof FormGroup) {
      const groupControls: Record<string, AbstractControl> = {};
      for (const key in control.controls) {
        groupControls[key] = this.cloneFormControl(control.controls[key]);
      }
      return new FormGroup(groupControls, validators);
    }

    if (control instanceof FormArray) {
      const arrayControls = control.controls.map(child => this.cloneFormControl(child));
      return new FormArray(arrayControls, validators);
    }

    throw new Error('Unsupported control type');
  }

  /**
   * @description Checks if a value is unique within a FormArray group.
   * @summary Validates that the primary key value in a FormGroup is unique among all groups
   * in the parent FormArray. The uniqueness check behavior differs based on the operation type.
   * For both CREATE and UPDATE operations, it checks that no other group in the array has the same
   * primary key value.
   * @param {FormGroup} formGroup - The FormGroup to check for uniqueness
   * @param {OperationKeys} [operation=OperationKeys.CREATE] - The type of operation being performed
   * @param {number} [index] - The index of the current group within the FormArray
   * @return {boolean} True if the value is unique, false otherwise
   * @static
   */
  static isUniqueOnGroup(formGroup: FormGroup, operation: OperationKeys = OperationKeys.CREATE, index?: number): boolean {
    const formArray = formGroup.parent as FormArray;
    if(index === undefined || index === null)
      index = formArray.length - 1;
    const pk = this.getComponentPropsFromGroupArray(formArray, BaseComponentProps.PK as string) as string;
    const controlName = Object.keys(formGroup.controls)[0];

    if(controlName !== pk || !pk)
      return true;
    const controlValue = cleanSpaces(`${formGroup.get(pk)?.value}`, true);
    if(operation === OperationKeys.CREATE) {
      return !formArray.controls.some((group, i) => {
        const value = cleanSpaces(`${group.get(pk)?.value}`, true);
        return i !== index && value === controlValue;
      });
    }

    return !formArray.controls.some((group, i) => {
      const value = cleanSpaces(`${group.get(pk)?.value}`, true);
      return i !== index && value === controlValue;
    });
  }

  /**
   * @description Enables all controls within a FormGroup or FormArray.
   * @summary Recursively enables all form controls within the provided FormGroup or FormArray.
   * This is useful for making all controls interactive after they have been disabled.
   * @param {FormArray | FormGroup} formGroup - The FormGroup or FormArray to enable all controls for
   * @return {void}
   * @static
   */
  static enableAllGroupControls(formGroup: FormArray | FormGroup): void {
    Object.keys(formGroup.controls).forEach(key => {
      const control = formGroup.get(key);
      if (control instanceof FormArray) {
        control.controls.forEach(child => {
          if (child instanceof FormGroup) {
            child.enable({ emitEvent: false });
            child.updateValueAndValidity({ emitEvent: true });
          }
        });
      }

      if(control instanceof FormGroup) {
        control.enable({ emitEvent: false });
        control.updateValueAndValidity({ emitEvent: true });
      }
    });
  }

  /**
   * @description Adds a form control to a form group based on component properties.
   * @summary Creates and configures a FormControl within the specified FormGroup using the provided
   * component properties. Handles nested paths, multiple controls (FormArrays), and control registration.
   * This method supports complex form structures with nested groups and arrays. It also manages
   * page-based forms and FormArray indexing.
   * @param {FormParent} formGroup - The form group or form array to add the control to
   * @param {IFormComponentProperties} componentProps - The component properties defining the control configuration
   * @param {Partial<IFormComponentProperties>} [parentProps={}] - Properties from the parent component for context
   * @param {number} [index=0] - The index for multiple controls in FormArrays
   * @return {FormParent} The updated form parent (FormGroup or FormArray)
   * @private
   * @static
   */
  private static addFormControl(formGroup: FormParent, componentProps: IFormComponentProperties, parentProps: Partial<IFormComponentProperties> = {}, index: number = 0): FormParent {

    const isMultiple = parentProps?.['multiple'] || parentProps?.['type'] === 'Array' || false;
    const { name, childOf } = componentProps;
    if(isMultiple)
      componentProps['pk'] = componentProps['pk'] || parentProps?.['pk'] || '';
    const fullPath = childOf ? (isMultiple ? `${childOf}.${index}.${name}` : `${childOf}.${name}`) : name;
    const [parentGroup, controlName] = this.resolveParentGroup(formGroup as FormGroup, fullPath, componentProps, parentProps);

    if (!parentGroup.get(controlName)) {
      const control = NgxFormService.fromProps(
        componentProps,
        componentProps.updateMode || 'change',
      );
      NgxFormService.register(control, componentProps);
      if (parentGroup instanceof FormGroup) {
        parentGroup.addControl(controlName, control);
      }
      if(parentGroup instanceof FormArray) {
        const root = parentGroup.controls[(componentProps as KeyValue)?.['page'] - 1] as FormGroup;
        if(root) {
           root.addControl(controlName, control);
        } else {
          parentGroup.push({[controlName]: control});
        }
      }
    }
    let rootGroup = parentGroup;
    if(rootGroup instanceof FormArray)
      rootGroup = (parentGroup as FormArray).controls[(componentProps as KeyValue)?.['page'] - 1] as FormParent;
    // if(childOf?.includes("."))
    //   rootGroup = (rootGroup as FormGroup)?.parent as FormArray

    // componentProps['formGroup'] = root as FormGroup;
    componentProps['formGroup'] = rootGroup as FormGroup;
    componentProps['formControl'] = parentGroup.get(controlName) as FormControl;
    // componentProps['multiple'] = isMultiple;
    return parentGroup as FormParent;
  }

  /**
   * @description Retrieves a control from a registered form.
   * @summary Finds and returns an AbstractControl from a registered form using the form id and optional path.
   * This method provides centralized access to form controls across the application by leveraging
   * the form registry system.
   * @param {string} formId - The unique identifier of the form in the registry
   * @param {string} [path] - The optional dot-separated path to a specific control within the form
   * @return {AbstractControl} The requested AbstractControl (FormGroup, FormArray, or FormControl)
   * @throws {Error} If the form is not found in the registry or the control is not found in the form
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant FR as Form Registry
   *   C->>NFS: getControlFromForm(formId, path?)
   *   NFS->>FR: Get form by formId
   *   alt Form not found
   *     FR-->>NFS: null
   *     NFS-->>C: Throw Error
   *   else Form found
   *     FR-->>NFS: Return form
   *     alt path provided
   *       NFS->>NFS: form.get(path)
   *       alt Control not found
   *         NFS-->>C: Throw Error
   *       else
   *         NFS-->>C: Return control
   *       end
   *     else
   *       NFS-->>C: Return form
   *     end
   *   end
   * @static
   */
  static getControlFromForm(formId: string, path?: string): AbstractControl {
    const form = this.formRegistry.get(formId);
    if (!form)
      throw new Error(`Form with id '${formId}' not found in the registry.`);

    if (!path)
      return form;

    const control = form.get(path);
    if (!control)
      throw new Error(`Control with path '${path}' not found in form '${formId}'.`);
    return control;
  }


  /**
   * @description Creates a form from UI model metadata children.
   * @summary Generates a FormGroup from an array of UIModelMetadata objects, extracting component
   * properties and creating appropriate form controls. This method is specifically designed to work
   * with the UI decorator system and provides automatic form generation from metadata.
   * @param {string} id - Unique identifier for the form
   * @param {boolean} [registry=false] - Whether to register the created form in the global registry
   * @param {UIModelMetadata[]} [children] - Array of UI model metadata objects to create controls from
   * @return {FormGroup} The created FormGroup with controls for each child metadata
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant AF as Angular Forms
   *   C->>NFS: createFormFromChildren(id, registry, children)
   *   NFS->>AF: new FormGroup({})
   *   loop For each child metadata
   *     NFS->>NFS: addFormControl(form, child.props)
   *     NFS->>AF: Create and add FormControl
   *   end
   *   alt registry is true
   *     NFS->>NFS: addRegistry(id, form)
   *   end
   *   NFS-->>C: Return FormGroup
   * @static
   */
  static createFormFromChildren(id: string, registry: boolean = false,  children?: UIModelMetadata[],): FormGroup {
    const form = new FormGroup({});
    if(children?.length)
      children.forEach(child => {
        this.addFormControl(form, child.props as IFormComponentProperties);
      });
    if (registry)
      this.addRegistry(id, form);
    return form;
  }

  /**
   * @description Creates a form from component configurations.
   * @summary Generates a FormGroup based on an array of component configurations and optionally registers it.
   * This method processes component input configurations to create appropriate form controls with
   * validation and initial values.
   * @param {string} id - The unique identifier for the form
   * @param {IComponentConfig[]} components - An array of component configurations defining the form structure
   * @param {boolean} [registry=false] - Whether to register the created form in the global registry
   * @return {FormGroup} The created FormGroup with controls for each component configuration
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant AF as Angular Forms
   *   C->>NFS: createFormFromComponents(id, components, registry)
   *   NFS->>AF: new FormGroup({})
   *   loop For each component config
   *     NFS->>NFS: addFormControl(form, component.inputs)
   *     NFS->>AF: Create and add FormControl
   *   end
   *   alt registry is true
   *     NFS->>NFS: addRegistry(id, form)
   *   end
   *   NFS-->>C: Return FormGroup
   * @static
   */
  static createFormFromComponents(id: string, components: IComponentConfig[], registry: boolean = false): FormGroup {
    const form = new FormGroup({});
    components.forEach(component => {
      this.addFormControl(form, component.inputs);
    });
    if (registry)
      this.addRegistry(id, form);
    return form;
  }

  /**
   * @description Adds a control to a form based on component properties.
   * @summary Creates and adds a form control to a form (existing or new) based on the provided component properties.
   * Handles multi-page forms by managing FormArray structures and proper indexing. This method supports
   * complex form scenarios including nested controls and page-based form organization. It automatically
   * creates FormArrays for forms with multiple pages and manages page indexing.
   * @param {string} id - The unique identifier of the form
   * @param {ComponentProperties} props - The properties of the component to create the control from
   * @param {props} [parentProps] - Optional parent properties for context and configuration
   * @return {FormParent} The form or created control (FormGroup or FormArray)
   * @throws {Error} If page property is required but not provided or is invalid
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant F as Form
   *   C->>NFS: addControlFromProps(id, props, parentProps?)
   *   NFS->>NFS: createForm(id, formArray, true)
   *   alt Multi-page form (parentProps.pages > 0)
   *     NFS->>NFS: Calculate page index
   *     alt Group doesn't exist at index
   *       NFS->>F: Create new FormGroup at index
   *     end
   *     NFS->>NFS: Set form to page FormGroup
   *   end
   *   alt props has path
   *     NFS->>NFS: addFormControl(form, props, parentProps)
   *   end
   *   NFS-->>C: Return form/control
   * @static
   */
  static addControlFromProps(id: string, props: IFormComponentProperties, parentProps?: IFormComponentProperties): FormParent {

    const componentPages = (typeof props?.pages === Primitives.NUMBER ?
      props?.pages : (props?.pages as IPagedComponentProperties[])?.length) as number;
    const parentPages = (typeof  parentProps?.pages === Primitives.NUMBER ?
    parentProps?.pages : (parentProps?.pages as IPagedComponentProperties[])?.length) as number;

    const isFormArray = (componentPages && componentPages  >= 1 || props.multiple === true);
    let form = this.createForm(id, isFormArray, true);

    if(parentPages && parentPages > 0) {
      const childOf = props.childOf || "";
      const parentChildOf = parentProps?.childOf || "";
      const index = props.page || parentProps?.page;
      // dont check page in nested childs with same childOf
      if((!(typeof index === 'number') || index === 0) && (childOf.length && childOf !== parentChildOf))
        throw Error(`Property 'page' is required and greather than 0 on ${props.name}`);
      // if(index > formLength) {
      //   if((form as KeyValue)?.['lastIndex'] && index === (form as KeyValue)['lastIndex']['page']) {
      //     index = (form as KeyValue)['lastIndex']['index'];
      //   } else {
      //     (form as KeyValue)['lastIndex'] = {
      //       page: index,
      //       index: formLength + 1
      //     };
      //     index = formLength + 1;

      //   }
      // }
      let group = (form as FormArray).controls[(index as number) - 1];
      if(!group) {
        group = new FormGroup({});
        (form as FormArray).insert(index as number, group);
      }
      form = group as FormGroup;
    }
    if(props.path)
      form = this.addFormControl(form, props, parentProps);
    return form;
  }

  /**
   * @description Retrieves form data from a FormGroup.
   * @summary Extracts and processes the data from a FormGroup, handling different input types and nested form groups.
   * Performs type conversion for various HTML5 input types, validates nested controls, and manages
   * multiple control scenarios. Automatically enables all group controls after data extraction.
   * @param {FormGroup} formGroup - The FormGroup to extract data from
   * @return {Record<string, unknown>} An object containing the processed form data with proper type conversions
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant FG as FormGroup
   *   participant FC as FormControl
   *   C->>NFS: getFormData(formGroup)
   *   loop For each control in formGroup
   *     alt Control is not FormControl
   *       NFS->>NFS: Recursive getFormData(control)
   *       alt parentProps.multiple and !isValid
   *         NFS->>NFS: reset(control)
   *       end
   *     else Control is FormControl
   *       NFS->>FC: Get control value
   *       NFS->>NFS: Apply type conversion based on props.type
   *       alt HTML5CheckTypes
   *         NFS->>NFS: Keep boolean value
   *       else NUMBER type
   *         NFS->>NFS: parseToNumber(value)
   *       else DATE/DATETIME types
   *         NFS->>NFS: new Date(value)
   *       else Other types
   *         NFS->>NFS: escapeHtml(value)
   *       end
   *     end
   *   end
   *   NFS->>NFS: enableAllGroupControls(formGroup)
   *   NFS-->>C: Return processed data object
   * @static
   */
  static getFormData(formGroup: FormGroup): Record<string, unknown> {
    const data: Record<string, unknown> = {};
    for (const key in formGroup.controls) {
      const control = formGroup.controls[key];
      const parentProps = NgxFormService.getPropsFromControl(formGroup as FormGroup | FormArray);
      if (!(control instanceof FormControl)) {
        if(control.disabled) {
          if(control instanceof FormGroup)
            data[key] = NgxFormService.getFormData(control as FormGroup);
          if(control instanceof FormArray) {
            const value = (control as FormArray).controls.map(c => {
              if(Object.values(c.value).some(p => p !== undefined))
                return c.value;
              return undefined;
            }).filter(v => v !== undefined);
            if(Array.isArray(value)) {
              if(value.length > 0) {
                data[key] = value.reduce((acc, curr, i) => {
                  acc[i] = curr;
                  return acc;
                }, {});
              }
              continue;
            }
            data[key] = value;
          }
          continue;
        }
        const value = NgxFormService.getFormData(control as FormGroup);
        const isValid = control.valid;
        if(parentProps.multiple) {
          if(isValid) {
              data[key] = value;
          } else {
            this.reset(control as FormControl);
          }
          continue;
        }
        data[key] = value;
        continue;
      }

      const props = NgxFormService.getPropsFromControl(control as FormControl | FormArray);
      let value = control.value;
      if (!HTML5CheckTypes.includes(props['type'])) {
        switch (props['type']) {
          case HTML5InputTypes.NUMBER:
            value = parseToNumber(value);
            break;
          case HTML5InputTypes.DATE:
          case HTML5InputTypes.DATETIME_LOCAL:
            value = new Date(value);
            break;
          default:
            value = escapeHtml(value);
        }
      } else {
        if(props['type'] === HTML5InputTypes.CHECKBOX && Array.isArray(value))
          value = control.value;
      }
      data[key] = value;
    }
    NgxFormService.enableAllGroupControls(formGroup as FormGroup);
    return data;
  }

  /**
   * @description Validates fields in a form control or form group.
   * @summary Recursively validates all fields in a form control or form group, marking them as touched and dirty.
   * Performs comprehensive validation including uniqueness checks for primary keys in FormArray scenarios.
   * This method ensures all validation rules are applied and form state is properly updated.
   * @param {AbstractControl} control - The control or form group to validate
   * @param {string} [pk] - Optional primary key field name for uniqueness validation
   * @param {string} [path] - The path to the control within the form for error reporting
   * @return {boolean} True if all fields are valid, false otherwise
   * @throws {Error} If no control is found at the specified path or if the control type is unknown
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant FC as FormControl
   *   participant FG as FormGroup
   *   participant FA as FormArray
   *   C->>NFS: validateFields(control, pk?, path?)
   *   alt Control is FormControl
   *     NFS->>FC: markAsTouched()
   *     NFS->>FC: markAsDirty()
   *     NFS->>FC: updateValueAndValidity()
   *     alt Is in FormArray group
   *       NFS->>NFS: Check uniqueness in group
   *       alt Not unique
   *         NFS->>FC: setErrors({notUnique: true})
   *       end
   *     end
   *     NFS-->>C: Return control.valid
   *   else Control is FormGroup
   *     loop For each child control
   *       NFS->>NFS: Recursive validateFields(child)
   *     end
   *     NFS-->>C: Return allValid
   *   else Control is FormArray
   *     loop For each array control
   *       NFS->>NFS: Recursive validateFields(child)
   *     end
   *     NFS-->>C: Return allValid
   *   else Unknown control type
   *     NFS-->>C: Throw Error
   *   end
   * @static
   */
  static validateFields(control: AbstractControl, pk?: string,  path?: string): boolean {
    control = path ? control.get(path) as AbstractControl : control;
    if (!control)
      throw new Error(`No control found at path: ${path || 'root'}.`);

    const isAllowed = [FormArray, FormGroup, FormControl].some(type => control instanceof type);
    if (!isAllowed)
      throw new Error(`Unknown control type at: ${path || 'root'}`);

    control.markAsTouched();
    control.markAsDirty();
    control.updateValueAndValidity({emitEvent: true });

    if (control instanceof FormGroup) {
      Object.values(control.controls).forEach(childControl => {
        this.validateFields(childControl);
      });
    }

    if (control instanceof FormArray) {
      const totalGroups = control.length;
      const hasValid = control.controls.some(control => control.valid);
      const parentProps = (control as KeyValue)[BaseComponentProps.FORM_GROUP_COMPONENT_PROPS] || {};
      const childControl = control.at(0);
      if(totalGroups === 1) {
        const parent = childControl.parent as FormGroup;
        if(!parentProps.required) {
          parent.setErrors(null);
          parent.updateValueAndValidity({ emitEvent: true });
          childControl.disable();
        } else {
          this.validateFields(childControl);
        }
      }
      else if(totalGroups > 1 && hasValid) {
         for (let i = control.length - 1; i >= 0; i--) {
          const childControl = control.at(i);
          // disable no valid groups on array
          if (!childControl.valid) {
            const parent = childControl.parent as FormGroup;
            parent.setErrors(null);
            parent.updateValueAndValidity({ emitEvent: true });
            childControl.disable();
          } else {
            this.validateFields(childControl);
          }
        }
      } else {
        Object.values(control.controls).forEach(childControl => {
          this.validateFields(childControl);
        });
      }
    }

    // function getControlName(control: AbstractControl): string | null {
    //   const group = control.parent as FormGroup;
    //   if (!group)
    //       return null;
    //   return Object.keys(group.controls).find(name => control === group.get(name)) || null;
    // }

    return control?.disabled ? true :  control.valid;
  }

  /**
   * @description Generates validators from component properties.
   * @summary Creates an array of ValidatorFn based on the supported validation keys in the component properties.
   * Maps each validation property to its corresponding Angular validator function using the ValidatorFactory.
   * Only processes properties that are recognized as validation keys by the Validation utility.
   * @param {KeyValue} props - The component properties containing validation rules
   * @return {ValidatorFn[]} An array of validator functions
   * @private
   * @static
   */
  private static validatorsFromProps(props: KeyValue): ValidatorFn[] {
    const supportedValidationKeys = Validation.keys();
    return Object.keys(props)
      .filter((k: string) => supportedValidationKeys.includes(k))
      .map((k: string) => {
        return ValidatorFactory.spawn(props as FieldProperties, k);
      });
  }

  /**
   * @description Creates a FormControl from component properties.
   * @summary Generates a FormControl with validators and initial configuration based on the provided
   * component properties. Handles different input types, sets initial values, and configures
   * validation rules and update modes.
   * @param {FieldProperties} props - The component properties defining the control configuration
   * @param {FieldUpdateMode} [updateMode='change'] - The update mode for the control ('change', 'blur', 'submit')
   * @return {FormControl} The created FormControl with proper configuration and validators
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant VF as ValidatorFactory
   *   participant AF as Angular Forms
   *   C->>NFS: fromProps(props, updateMode?)
   *   NFS->>NFS: validatorsFromProps(props)
   *   NFS->>VF: Create validators from props
   *   VF-->>NFS: Return validator array
   *   NFS->>NFS: Compose validators
   *   alt props.value exists and not checkbox
   *     alt props.type is DATE
   *       NFS->>NFS: Validate date format
   *     end
   *     NFS->>NFS: Set initial value
   *   end
   *   NFS->>AF: new FormControl(config)
   *   AF-->>NFS: Return FormControl
   *   NFS-->>C: Return configured FormControl
   * @static
   */
  static fromProps(props: FieldProperties, updateMode: FieldUpdateMode = 'change'): FormControl {
    const validators = this.validatorsFromProps(props);
    const composed = validators.length ? Validators.compose(validators) : null;
    return new FormControl(
      {
        value:
          props.value
          ? props.type === HTML5InputTypes.CHECKBOX ?
            Array.isArray(props.value) ? props.value : undefined
            : props.type === HTML5InputTypes.DATE
              ? !isValidDate(parseDate(props.format as string, props.value as string))
                ? undefined : props.value :
              (props.value as unknown) : undefined,
        disabled: props.disabled,
      },
      {
        validators: composed,
        updateOn: updateMode,
      },
    );
  }

  /**
   * @description Retrieves properties from a FormControl, FormArray, or FormGroup.
   * @summary Gets the FieldProperties associated with a form control from the internal WeakMap.
   * This method provides access to the original component properties that were used to create
   * the control, enabling validation, rendering, and behavior configuration.
   * @param {FormControl | FormArray | FormGroup} control - The form control to get properties for
   * @return {FieldProperties} The properties associated with the control, or empty object if not found
   * @static
   */
  static getPropsFromControl(control: FormControl | FormArray | FormGroup): FieldProperties {
    return this.controls.get(control) || {} as FieldProperties;
  }

  /**
   * @description Finds a parent element with a specific tag in the DOM tree.
   * @summary Traverses up the DOM tree to find the nearest parent element with the specified tag name.
   * This is useful for finding container elements or specific parent components in the DOM hierarchy.
   * The search is case-insensitive for tag name matching.
   * @param {HTMLElement} el - The starting element to traverse from
   * @param {string} tag - The tag name to search for (case-insensitive)
   * @return {HTMLElement} The found parent element with the specified tag
   * @throws {Error} If no parent with the specified tag is found in the DOM tree
   * @mermaid
   * sequenceDiagram
   *   participant C as Component
   *   participant NFS as NgxFormService
   *   participant DOM as DOM Tree
   *   C->>NFS: getParentEl(element, tagName)
   *   loop Traverse up DOM tree
   *     NFS->>DOM: Get parentElement
   *     DOM-->>NFS: Return parent or null
   *     alt Parent exists and tag matches
   *       NFS-->>C: Return parent element
   *     else Parent is null
   *       NFS-->>C: Throw Error
   *     end
   *   end
   * @static
   */
  static getParentEl(el: HTMLElement, tag: string): HTMLElement {
    let parent: HTMLElement | null;
    while ((parent = el.parentElement) !== null) {
      if (parent.tagName.toLowerCase() === tag.toLowerCase()) {
        return parent;
      }
      el = parent;
    }
    throw new Error(
      `No parent with the tag ${tag} was found for provided element`,
    );
  }

  /**
   * @description Registers a control with its properties in the internal WeakMap.
   * @summary Associates a form control with its component properties for later retrieval.
   * This enables the service to maintain metadata about controls without creating memory leaks,
   * as WeakMap automatically cleans up references when controls are garbage collected.
   * @param {AbstractControl} control - The control to register (FormControl, FormGroup, or FormArray)
   * @param {FieldProperties} props - The properties to associate with the control
   * @return {void}
   * @static
   */
  static register(control: AbstractControl, props: FieldProperties): void {
    this.controls.set(control, props);
  }

  /**
   * @description Unregisters a control from the internal WeakMap.
   * @summary Removes a control and its associated properties from the internal WeakMap.
   * This cleans up the metadata tracking for the control and frees up memory. Returns
   * true if the control was found and removed, false if it was not in the registry.
   * @param {AbstractControl} control - The control to unregister (FormControl, FormGroup, or FormArray)
   * @return {boolean} True if the control was successfully unregistered, false otherwise
   * @static
   */
  static unregister(control: AbstractControl): boolean {
    return this.controls.delete(control);
  }

  /**
   * @description Resets a form group or form control.
   * @summary Recursively resets all controls in a form group or a single form control, clearing values,
   * errors, and marking them as pristine and untouched. For FormControls, it sets the value to empty
   * string (except for checkbox types) and clears validation errors. For FormGroups, it recursively
   * resets all child controls.
   * @param {FormGroup | FormControl} formGroup - The form group or form control to reset
   * @return {void}
   * @static
   */
  static reset(formGroup: FormGroup | FormControl): void {
    if(formGroup instanceof FormControl) {
      const control = formGroup as FormControl;
      const { type } = NgxFormService.getPropsFromControl(control);
      if (!HTML5CheckTypes.includes(type))
      control.setValue("");
      control.markAsPristine();
      control.markAsUntouched();
      control.setErrors(null);
      control.updateValueAndValidity();
    } else {
      for (const key in formGroup.controls) {
        const control = formGroup.controls[key];
        NgxFormService.reset(control as FormControl);
        continue;
      }
    }
  }
}