Source

lib/engine/NgxCrudFormField.ts

import { CrudOperationKeys, FieldProperties, RenderingError } from '@decaf-ts/ui-decorators';
import { KeyValue, PossibleInputTypes } from './types';
import { CrudOperations, InternalError, OperationKeys } from '@decaf-ts/db-decorators';
import { ControlValueAccessor, FormControl, FormGroup } from '@angular/forms';
import { ElementRef, inject } from '@angular/core';
import { NgxFormService } from './NgxFormService';
import { sf } from '@decaf-ts/decorator-validation';
import { TranslateService } from '@ngx-translate/core';
import { EventConstants } from './constants';

/**
 * @class NgxCrudFormField
 * @implements {FieldProperties}
 * @implements {ControlValueAccessor}
 * @summary Abstract class representing a CRUD form field for Angular applications
 * @description This class provides the base implementation for CRUD form fields in Angular,
 * implementing both CrudFormField and ControlValueAccessor interfaces.
 */
export abstract class NgxCrudFormField implements ControlValueAccessor, FieldProperties {
  /**
   * @summary Reference to the component's element
   * @description ElementRef representing the component's native element
   */
  component!: ElementRef;

  /**
   * @summary Current CRUD operation
   * @description Represents the current CRUD operation being performed
   */
  operation!: CrudOperations;

  /**
   * @summary Form group for the field
   * @description Angular FormGroup instance for the field
   */
  formGroup!: FormGroup | undefined;

  formControl!: FormControl;

  name!: string;

  path!: string;

  childOf?: string;

  type!: PossibleInputTypes;

  disabled?: boolean;

  uid?: string;

  // Validation

  format?: string;
  hidden?: boolean | CrudOperationKeys[];
  max?: number | Date;
  maxlength?: number;
  min?: number | Date;
  minlength?: number;
  pattern?: string | undefined;
  readonly?: boolean;
  required?: boolean;
  step?: number;
  equals?: string;
  different?: string;
  lessThan?: string;
  lessThanOrEqual?: string;
  greaterThan?: string;
  greaterThanOrEqual?: string;

  value!: string | number | Date;

  multiple!: boolean;

  private translateService = inject(TranslateService);

  private validationErrorEventDispateched: boolean = false;

  /**
   * @summary Parent HTML element
   * @description Reference to the parent HTML element of the field
   */
  protected parent?: HTMLElement;

  // protected constructor() {}

  /**
   * @summary String formatting function
   * @description Provides access to the sf function for error message formatting
   * @prop {function(string, ...string): string} sf - String formatting function
   */
  sf = sf;

  /**
   * @summary Change callback function
   * @description Function called when the field value changes
   * @property {function(): unknown} onChange - onChange event handler
   */
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onChange: () => unknown = () => {};

  /**
   * @summary Touch callback function
   * @description Function called when the field is touched
   * @property {function(): unknown} onTouch - onTouch event handler
   */
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  onTouch: () => unknown = () => {};

  /**
   * @summary Write value to the field
   * @description Sets the value of the field
   * @param {string} obj - The value to be set
   */
  writeValue(obj: string): void {
    this.value = obj;
  }

  /**
   * @summary Register change callback
   * @description Registers a function to be called when the field value changes
   * @param {function(): unknown} fn - The function to be called on change
   */
  registerOnChange(fn: () => unknown): void {
    this.onChange = fn;
  }

  /**
   * @summary Register touch callback
   * @description Registers a function to be called when the field is touched
   * @param {function(): unknown} fn - The function to be called on touch
   */
  registerOnTouched(fn: () => unknown): void {
    this.onTouch = fn;
  }

  /**
   * @summary Set disabled state
   * @description Sets the disabled state of the field
   * @param {boolean} isDisabled - Whether the field should be disabled
   */
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  /**
   * @summary After view initialization logic
   * @description Performs necessary setup after the view has been initialized
   * @returns {HTMLElement} The parent element of the field
   */
  afterViewInit(): HTMLElement {
    let parent: HTMLElement;
    switch (this.operation) {
      case OperationKeys.READ:
      case OperationKeys.DELETE:
        return this.component.nativeElement.parentElement;
      case OperationKeys.CREATE:
      case OperationKeys.UPDATE:
        try {
          parent = NgxFormService.getParentEl(this.component.nativeElement, 'div');
        } catch (e: unknown) {
          throw new RenderingError(`Unable to retrieve parent form element for the ${this.operation}: ${e instanceof Error ? e.message : e}`);
        }
        // NgxFormService.register(parent.id, this.formGroup, this as AngularFieldDefinition);
        return parent;
      default:
        throw new InternalError(`Invalid operation: ${this.operation}`);
    }
  }

  /**
   * @summary Cleanup on component destruction
   * @description Unregisters the field when the component is destroyed
   */
  onDestroy(): void {
    if(this.formGroup)
      NgxFormService.unregister(this.formGroup);
  }

  /**
   * @summary Get field errors
   * @description Retrieves all errors associated with the field
   * @returns {string|void} An array of error objects
   */
  getErrors(parent: HTMLElement): string | void {
    const formControl = this.formControl;
    const accordionComponent = parent.closest('ngx-decaf-fieldset')?.querySelector('ion-accordion-group');
    if((!formControl.pristine || formControl.touched) && !formControl.valid) {
      const errors: Record<string, string>[] = Object.keys(formControl.errors ?? {}).map(key => ({
        key: key,
        message: key,
      }));
      if(errors.length) {
        if(accordionComponent && !this.validationErrorEventDispateched) {
          const validationErrorEvent = new CustomEvent(EventConstants.VALIDATION_ERROR, {
            detail: {fieldName: this.name, hasErrors: true},
            bubbles: true
          });
          accordionComponent.dispatchEvent(validationErrorEvent);
          this.validationErrorEventDispateched = true;
        }
      }
      for(const error of errors)
        return `* ${this.sf(this.translateService.instant(`errors.${error?.['message']}`), (this as KeyValue)[error?.['key']] ?? "")}`;
    }
  }
}