### File

`src/lib/form-builder.ts`

### Index

###### Methods
Public inserted Public removed

### Constructor

`constructor(form: T, controlId)`
Defined in src/lib/form-builder.ts:37
Parameters :
Name | Type | Optional
form | `T` | No
controlId |  | No

### Methods

Public inserted
`inserted(index: number, controlOrDefinition: AbstractControl | FormDefinition)`
Defined in src/lib/form-builder.ts:41
Parameters :
Name | Type | Optional
index | `number` | No
controlOrDefinition | `AbstractControl | FormDefinition` | No
Returns : `void`

Public removed
`removed(index: number)`
Defined in src/lib/form-builder.ts:55
Parameters :
Name | Type | Optional
index | `number` | No
Returns : `void`

    import {
      Injector,
      isDevMode,
      StaticProvider,
    } from '@angular/core';
    import {
      assertIsArray,
      assertIsFunction,
      assertIsObject,
      Constructor,
      HasRxapOnInitMethod,
    } from '@rxap/utilities';
    import { MetadataKeys } from './decorators/metadata-keys';
    import {
      AbstractControl,
      AsyncValidatorFn,
      ValidatorFn,
      Validators,
    } from '@angular/forms';
    import {
      ChangeFn,
      FormDefinition,
      FormDefinitionMetadata,
      FormType,
      RxapAbstractControlOptions,
      RxapAbstractControlOptionsWithDefinition,
      SetValueFn,
    } from './model';
    import { RxapFormArray } from './form-array';
    import { RxapFormGroup } from './form-group';
    import { RxapFormControl } from './form-control';
    import { GetDefinitionMetadata } from '@rxap/definition';
    import { ControlSetValueOptions } from './decorators/control-set-value';
    import { getMetadata } from '@rxap/reflect-metadata';

    export class FormArrayControlManager<T extends FormDefinition> {
      constructor(private readonly form: T, private readonly controlId: keyof T) {
      }

      public inserted(
        index: number,
        controlOrDefinition: AbstractControl | FormDefinition,
      ): void {
        const controlContainer = this.form[this.controlId];
        if (Array.isArray(controlContainer)) {
          controlContainer.splice(index, 0, controlOrDefinition);
        } else {
          throw new Error(
            'Could not register inserted control or definition. The control id does not points to an array of control or definition instance!',
          );
        }
      }

      public removed(index: number): void {
        const controlContainer = this.form[this.controlId];
        if (Array.isArray(controlContainer)) {
          controlContainer.splice(index, 1);
        } else {
          throw new Error(
            'Could not register inserted control or definition. The control id does not points to an array of control or definition instance!',
          );
        }
      }
    }

    export class RxapFormBuilder<
      Data extends Record<string, any> = any,
      Form extends FormType<Data> = FormType<Data>
    > {
      private readonly formArrayGroups: Map<
        string,
        RxapAbstractControlOptionsWithDefinition
      >;
      private readonly formArrayControls: Map<string, RxapAbstractControlOptions>;
      private readonly formControls: Map<string, RxapAbstractControlOptions>;
      private readonly formGroups: Map<
        string,
        RxapAbstractControlOptionsWithDefinition
      >;
      private readonly formOptions: FormDefinitionMetadata;
      private readonly validators: Map<string, Set<string>>;
      private readonly asyncValidators: Map<string, Set<string>>;
      private readonly controlChanges: Map<string, Set<string>>;
      private readonly controlSetValue: Map<string, Set<ControlSetValueOptions>>;

      private readonly providers: StaticProvider[];

      constructor(
        private readonly definition: Constructor<Form>,
        private readonly injector: Injector = Injector.NULL,
        providers: StaticProvider[] = [],
      ) {
        this.formArrayGroups = this.extractArrayGroups();
        this.formArrayControls = this.extractArrayControls();
        this.formControls = this.extractControls();
        this.formGroups = this.extractGroups();
        this.validators = this.extractValidators();
        this.asyncValidators = this.extractAsyncValidators();
        this.controlChanges = this.extractControlChanges();
        this.controlSetValue = this.extractControlSetValue();
        const formOptions = (this.formOptions = this.extractFormOptions());

        // merge the providers from the from options with the providers from the
        // constructor. If the provider is defined in the form options and constructor
        // then the provider from the constructor will be used.
        this.providers = [ ...providers, ...(formOptions.providers ?? []) ].filter(
          (provider: any, index, self) =>
            provider['provide'] &&
            self.findIndex(
              (selfProvider: any) =>
                selfProvider.provide &&
                selfProvider.provide === provider.provide,
            ) === index,
        );
      }

      public build<T extends (FormDefinition<Data> | FormType<Data>)>(
        state: Readonly<any> = {},
        options: Partial<FormDefinitionMetadata & { controlId?: string }> = {},
      ): T {
        const injector = Injector.create({
          name: `rxap/form-builder/${ this.formOptions.id }`,
          parent: this.injector,
          providers: this.providers,
        });

        let form: Record<string, Function> & FormDefinition;

        // don't use the notFoundValue feature of the injector.
        // if used for each call of the get method an "empty" or "fallback"
        // instance is created

        try {
          form = injector.get(this.definition);
        } catch (e: any) {
          if (e.name === 'NullInjectorError') {
            if (e.message.includes(`No provider for ${ this.definition.name }`)) {
              if (isDevMode()) {
                console.warn(
                  `Could not inject the definition instance. Fallback and call the constructor of the definition class: ${ e.message }`);
              }
              form = new this.definition() as any;
            } else {
              throw e;
            }
          } else {
            throw new Error(`Could not inject the definition instance: ${ e.message }`);
          }
        }

        const controls: Record<string, AbstractControl> = {};

        this.buildControls(state, form, controls);
        this.buildGroups(state, form, controls);
        this.buildArrayGroups(state, form, controls);
        this.buildArrayControls(state, form, controls);

        const formOptions = {
          ...this.formOptions,
          controlId:
            options.controlId ?? (options.id ? options.id : this.formOptions.id),
          ...options,
        };

        form.rxapFormGroup = new RxapFormGroup(controls, formOptions);

        Reflect.set(form.rxapFormGroup, '_rxapFormDefinition', form);

        form.rxapMetadata = this.formOptions;

        if (HasRxapOnInitMethod(form)) {
          form.rxapOnInit();
        }

        for (const control of Object.values(form.rxapFormGroup.controls)) {
          if (control.controlId) {
            const controlSetValueOnInit = Array.from(
              this.controlSetValue.get(control.controlId) ?? [],
            ).filter((controlSetValueOptions) => controlSetValueOptions.initial);
            for (const onSetValue of this.coerceToFnArray<SetValueFn>(
              form,
              controlSetValueOnInit,
            )) {
              onSetValue(control.value, { initial: true });
            }
          }
        }

        return form as any;
      }

      private buildArrayControls(
        builderFormState: any,
        form: FormDefinition & Record<string, Function>,
        controls: Record<string, AbstractControl>,
      ): void {
        for (const [ controlId, options ] of this.formArrayControls.entries()) {
          const formState = this.coerceToArrayFormState(
            controlId,
            [],
            builderFormState,
            options.state,
          );

          assertIsArray(formState);

          const formControls: any = formState.map(
            (fs, index) => new RxapFormControl(fs, { controlId: index.toFixed(0) }),
          );

          const manager = new FormArrayControlManager(form, controlId);

          const formArray =
            (formControls.rxapFormArray =
              controls[controlId] =
                new RxapFormArray(
                  // TODO : add the formControlOptions property. To definition the form control options
                  formControls,
                  {
                    ...options,
                    controlId,
                    validators: [],
                    asyncValidators: [],
                    controlInsertedFn: manager.inserted.bind(manager),
                    controlRemovedFn: manager.removed.bind(manager),
                    builder: (state, controlOptions) =>
                      new RxapFormControl(state, controlOptions),
                  },
                ));

          // TODO : add support for injectable validators

          formArray.setValidators(
            this.coerceToValidatorArray(
              form,
              options.validators,
              this.validators.get(controlId),
            ),
            false,
          );
          formArray.setAsyncValidators(
            this.coerceToValidatorArray(
              form,
              options.asyncValidators,
              this.asyncValidators.get(controlId),
            ),
            false,
          );

          Reflect.set(form, controlId, formControls);
        }
      }

      private buildArrayGroups(
        builderFormState: any,
        form: FormDefinition & Record<string, Function>,
        controls: Record<string, AbstractControl>,
      ): void {
        for (const [ controlId, options ] of this.formArrayGroups.entries()) {
          const formState = this.coerceToArrayFormState(
            controlId,
            [],
            builderFormState,
            options.state,
          );

          assertIsArray(formState);

          const formGroupBuilder = new RxapFormBuilder(
            options.definition,
            this.injector,
            this.providers,
          );

          // TODO : add the formGroupOptions property. To definition of the form group options
          const formGroupDefinitions: any = formState.map((fs, index) =>
            formGroupBuilder.build(fs, { id: index.toFixed(0) }),
          );

          const manager = new FormArrayControlManager(form, controlId);

          const formArray =
            (formGroupDefinitions.rxapFormArray =
              controls[controlId] =
                new RxapFormArray(
                  formGroupDefinitions.map(
                    (fgd: FormDefinition) => fgd.rxapFormGroup,
                  ),
                  {
                    ...options,
                    builder: formGroupBuilder.build.bind(formGroupBuilder) as any,
                    controlId,
                    validators: [],
                    asyncValidators: [],
                    controlInsertedFn: manager.inserted.bind(manager),
                    controlRemovedFn: manager.removed.bind(manager),
                  },
                ));

          // TODO : add support for injectable validators

          formArray.setValidators(
            this.coerceToValidatorArray(
              form,
              options.validators,
              this.validators.get(controlId),
            ),
            false,
          );
          formArray.setAsyncValidators(
            this.coerceToValidatorArray(
              form,
              options.asyncValidators,
              this.asyncValidators.get(controlId),
            ),
            false,
          );

          Reflect.set(form, controlId, formGroupDefinitions);
        }
      }

      private buildGroups(
        builderFormState: any,
        form: Record<string, Function>,
        controls: Record<string, AbstractControl>,
      ): void {
        for (const [ controlId, options ] of this.formGroups.entries()) {
          const formState = this.coerceToGroupFormState(
            controlId,
            {},
            builderFormState,
            options.state,
          );

          assertIsObject(formState);

          const formGroupDefinition: FormDefinition = new RxapFormBuilder(
            options.definition,
            this.injector,
            this.providers,
          ).build(formState, {
            ...options,
            validators: [],
            asyncValidators: [],
          });

          controls[controlId] = formGroupDefinition.rxapFormGroup!;

          const formGroup = formGroupDefinition.rxapFormGroup;

          // TODO : add support for injectable validators

          formGroup.setValidators(
            this.coerceToValidatorArray(
              form,
              options.validators,
              this.validators.get(controlId),
            ),
            false,
          );
          formGroup.setAsyncValidators(
            this.coerceToValidatorArray(
              form,
              options.asyncValidators,
              this.asyncValidators.get(controlId),
            ),
            false,
          );

          // set the form group definition instance to the form definition instance
          Reflect.set(form, controlId, formGroupDefinition);
        }
      }

      private buildControls(
        builderFormState: any,
        form: Record<string, Function>,
        controls: Record<string, AbstractControl>,
      ): void {
        for (const [ controlId, options ] of this.formControls.entries()) {
          const control = (controls[controlId] = new (options.controlType ??
            RxapFormControl)(
            this.coerceToControlFormState(
              controlId,
              null,
              builderFormState,
              options.state,
            ),
            {
              ...options,
              controlId,
              validators: [],
              asyncValidators: [],
            },
          ));

          const injectValidators: ValidatorFn[] = [];
          const injectAsyncValidators: AsyncValidatorFn[] = [];

          for (const injectValidator of options.injectValidators ?? []) {
            const injectableValidator = this.injector.get(injectValidator);
            if (injectableValidator.validate) {
              injectValidators.push(
                injectableValidator.validate.bind(injectableValidator),
              );
            }
            if (injectableValidator.asyncValidate) {
              injectAsyncValidators.push(
                injectableValidator.asyncValidate.bind(injectableValidator),
              );
            }
          }

          control.setValidators(
            this.coerceToValidatorArray(
              form,
              options.validators,
              this.validators.get(controlId),
              injectValidators,
            ),
            false,
          );
          control.setAsyncValidators(
            this.coerceToValidatorArray(
              form,
              options.asyncValidators,
              this.asyncValidators.get(controlId),
              injectAsyncValidators,
            ),
            false,
          );

          // register all control on change function with the form control
          this.coerceToFnArray<ChangeFn>(
            form,
            this.controlChanges.get(controlId),
          ).forEach((changeFn) => control.registerOnChange(changeFn));

          // register all control on set value function with the form control
          this.coerceToFnArray<SetValueFn>(
            form,
            this.controlSetValue.get(controlId),
          ).forEach((setValueFn) => control.registerOnSetValue(setValueFn));

          if (Array.isArray(options.validators)) {
            if (
              options.validators.includes(Validators.required) ||
              options.validators.includes(Validators.requiredTrue)
            ) {
              Reflect.set(control, 'hasRequiredValidator', true);
            }
          }

          // set the form control instance to the form definition instance
          Reflect.set(form, controlId, control);
        }
      }

      /**
       * Coerce to a function array.
       *
       * @param form the current form definition instance
       * @param methodKeys A set of propertyKeys that points to form
       * definition instance methods
       */
      private coerceToFnArray<T extends Function>(
        form: Record<string, Function>,
        methodKeys?: Iterable<string | { propertyKey: string }>,
      ): Array<T> {
        const changes: Array<T> = [];

        if (methodKeys) {
          for (const propertyKeyOrOptions of methodKeys) {
            const propertyKey =
              typeof propertyKeyOrOptions === 'string'
                ? propertyKeyOrOptions
                : propertyKeyOrOptions.propertyKey;
            assertIsFunction(form[propertyKey]);
            changes.push((form[propertyKey] as T).bind(form));
          }
        }

        return changes;
      }

      /**
       * Coerce to a validator function array.
       *
       * @param form the current form definition instance
       * @param optionsValidators The options validator functions
       * @param validatorMethodKeys A set of propertyKeys that points to form
       * definition instance methods
       * @param injectValidators Injected validator functions
       */
      private coerceToValidatorArray<VF extends ValidatorFn | AsyncValidatorFn>(
        form: Record<string, Function>,
        optionsValidators?: Array<VF> | VF | null,
        validatorMethodKeys?: Set<string>,
        injectValidators?: Array<VF>,
      ): Array<VF> {
        const validators: Array<VF> = (
          optionsValidators ? Array.isArray(optionsValidators) ? optionsValidators : [ optionsValidators ] : []
        ).slice();

        if (validatorMethodKeys) {
          validatorMethodKeys.forEach((propertyKey) => {
            assertIsFunction(form[propertyKey]);

            validators.push(form[propertyKey].bind(form) as VF);
          });
        }

        if (injectValidators) {
          validators.push(...injectValidators);
        }

        return validators;
      }

      /**
       * Coerce to the form state for the specified controlId. If none form state is
       * found the default form state will be returned.
       *
       * @param controlId A control id
       * @param defaultFormState The default form state
       * @param builderFormState The form state provides as build state parameter
       * @param optionsFormState The form state set by the decorator in the form definition
       */
      private coerceToControlFormState(
        controlId: string,
        defaultFormState: any,
        builderFormState: Record<string, any>,
        optionsFormState?: any,
      ): any {
        let formState = defaultFormState;

        if (builderFormState && builderFormState[controlId] !== undefined) {
          formState = builderFormState[controlId];
        } else if (optionsFormState !== undefined) {
          formState = optionsFormState ?? null;
        }

        if (typeof formState === 'function') {
          formState = formState();
        }

        return formState;
      }

      /**
       * Coerce to the form state for the specified controlId. If none form state is
       * found the default form state will be returned.
       *
       * The difference to the method coerceToControlFormState is that null will be
       * handled as undefined and the defaultFormState is used
       *
       * @param controlId A control id
       * @param defaultFormState The default form state
       * @param builderFormState The form state provides as build state parameter
       * @param optionsFormState The form state set by the decorator in the form definition
       */
      private coerceToGroupFormState(
        controlId: string,
        defaultFormState: any,
        builderFormState: Record<string, any>,
        optionsFormState?: any,
      ): any {
        let formState = defaultFormState;

        if (builderFormState && builderFormState[controlId]) {
          formState = builderFormState[controlId];
        } else if (optionsFormState) {
          formState = optionsFormState;
        }

        if (typeof formState === 'function') {
          formState = formState();
          if (!formState) {
            throw new Error(`The form state function for the form group '${ controlId }' returns a empty value (null/undefined)`);
          }
          if (typeof formState !== 'object') {
            throw new Error(`The form state function for the form group '${ controlId }' returns a non object like value instead '${ typeof formState }'`);
          }
        }

        return formState;
      }

      /**
       * Coerce to the form state for the specified controlId. If none form state is
       * found the default form state will be returned.
       *
       * The difference to the method coerceToControlFormState is that null will be
       * handled as undefined and the defaultFormState is used
       *
       * @param controlId A control id
       * @param defaultFormState The default form state
       * @param builderFormState The form state provides as build state parameter
       * @param optionsFormState The form state set by the decorator in the form definition
       */
      private coerceToArrayFormState(
        controlId: string,
        defaultFormState: any[] | (() => any[]),
        builderFormState: Record<string, any>,
        optionsFormState?: any,
      ): any[] {
        let formState = defaultFormState;

        if (builderFormState && builderFormState[controlId] && Array.isArray(builderFormState[controlId])) {
          formState = builderFormState[controlId];
        } else if (optionsFormState && Array.isArray(optionsFormState)) {
          formState = optionsFormState;
        }

        if (typeof formState === 'function') {
          formState = formState();
          if (!formState) {
            throw new Error(`The form state function for the form array '${ controlId }' returns a empty value (null/undefined)`);
          }
          if (!Array.isArray(formState)) {
            throw new Error(`The form state function for the form array '${ controlId }' returns a non array like value instead '${ typeof formState }'`);
          }
        }

        return formState;
      }

      private extractControlChanges(): Map<string, Set<string>> {
        return (
          getMetadata(MetadataKeys.CONTROL_CHANGES, this.definition.prototype) ??
          new Map<string, Set<string>>()
        );
      }

      private extractControlSetValue(): Map<string, Set<ControlSetValueOptions>> {
        return (
          getMetadata(MetadataKeys.CONTROL_SET_VALUE, this.definition.prototype) ??
          new Map<string, Set<ControlSetValueOptions>>()
        );
      }

      private extractValidators(): Map<string, Set<string>> {
        return (
          getMetadata(MetadataKeys.CONTROL_VALIDATORS, this.definition.prototype) ??
          new Map<string, Set<string>>()
        );
      }

      private extractAsyncValidators(): Map<string, Set<string>> {
        return (
          getMetadata(
            MetadataKeys.CONTROL_ASYNC_VALIDATORS,
            this.definition.prototype,
          ) ?? new Map<string, Set<string>>()
        );
      }

      private extractArrayGroups(): Map<
        string,
        RxapAbstractControlOptionsWithDefinition
      > {
        return (
          getMetadata(MetadataKeys.FORM_ARRAY_GROUPS, this.definition.prototype) ??
          new Map<string, RxapAbstractControlOptionsWithDefinition>()
        );
      }

      private extractArrayControls(): Map<string, RxapAbstractControlOptions> {
        return (
          getMetadata(
            MetadataKeys.FORM_ARRAY_CONTROLS,
            this.definition.prototype,
          ) ?? new Map<string, RxapAbstractControlOptions>()
        );
      }

      private extractGroups(): Map<
        string,
        RxapAbstractControlOptionsWithDefinition
      > {
        return (
          getMetadata(MetadataKeys.FORM_GROUPS, this.definition.prototype) ??
          new Map<string, RxapAbstractControlOptionsWithDefinition>()
        );
      }

      private extractControls(): Map<string, RxapAbstractControlOptions> {
        return (
          getMetadata(MetadataKeys.FORM_CONTROLS, this.definition.prototype) ??
          new Map<string, RxapAbstractControlOptions>()
        );
      }

      private extractFormOptions(): FormDefinitionMetadata {
        const options = GetDefinitionMetadata(this.definition);
        if (!options || !options['id']) {
          throw new Error('Ensure that the @UseForm decorator is used.');
        }
        if (!options['providers']) {
          options['providers'] = [];
        }
        return options;
      }
    }

# results matching ""

# No results matching ""