File

src/lib/directives/form.directive.ts

Extends

FormGroupDirective

Implements

OnInit OnChanges OnDestroy

Metadata

Index

Properties
Methods
Inputs
Outputs
HostBindings
Accessors

Constructor

constructor(cdr: ChangeDetectorRef, formDefinition: FormDefinition | null, submitMethod: FormSubmitMethod<any> | null, loadMethod: FormLoadMethod | null, loadFailedMethod: FormLoadFailedMethod | null, loadSuccessfulMethod: FormLoadSuccessfulMethod | null, submitFailedMethod: FormSubmitFailedMethod | null, submitSuccessfulMethod: FormSubmitSuccessfulMethod | null, formDefinitionBuilder: RxapFormBuilder | null, loadingIndicatorService: LoadingIndicatorService | null)
Parameters :
Name Type Optional
cdr ChangeDetectorRef No
formDefinition FormDefinition | null No
submitMethod FormSubmitMethod<any> | null No
loadMethod FormLoadMethod | null No
loadFailedMethod FormLoadFailedMethod | null No
loadSuccessfulMethod FormLoadSuccessfulMethod | null No
submitFailedMethod FormSubmitFailedMethod | null No
submitSuccessfulMethod FormSubmitSuccessfulMethod | null No
formDefinitionBuilder RxapFormBuilder | null No
loadingIndicatorService LoadingIndicatorService | null No

Inputs

initial
Type : T
loadFailedMethod
Type : FormLoadFailedMethod | null
Default value : null
loadMethod
Type : FormLoadMethod | null
Default value : null
loadSuccessfulMethod
Type : FormLoadSuccessfulMethod | null
Default value : null
rxapForm
Type : FormDefinition | string
submitFailedMethod
Type : FormSubmitFailedMethod | null
Default value : null
submitMethod
Type : FormSubmitMethod<any> | null
Default value : null
submitSuccessfulMethod
Type : FormSubmitSuccessfulMethod | null
Default value : null

Outputs

invalidSubmit
Type : EventEmitter
rxapSubmit
Type : EventEmitter

Emits when the submit method is executed without errors. The result of the submit method is passed as event object.

If no submit method is defined then emit after the submit button is clicked.

submitSuccessful
Type : EventEmitter

HostBindings

class.rxap-loaded
Type : boolean
class.rxap-loading
Type : boolean
class.rxap-loading-error
Type : Error | null
class.rxap-submit-error
Type : Error | null
class.rxap-submitting
Type : boolean

Methods

Protected getSubmitValue
getSubmitValue()
Returns : T
Protected loadFailed
loadFailed(error: Error)
Parameters :
Name Type Optional
error Error No
Returns : void
Protected loadInitialState
loadInitialState(form: RxapFormGroup)
Parameters :
Name Type Optional
form RxapFormGroup No
Returns : void
Protected loadSuccessful
loadSuccessful(value: any)
Parameters :
Name Type Optional
value any No
Returns : void
Public onSubmit
onSubmit($event: Event)
Parameters :
Name Type Optional
$event Event No
Returns : boolean
Protected submit
submit()
Returns : void
Protected submitFailed
submitFailed(error: Error)
Parameters :
Name Type Optional
error Error No
Returns : void
Protected submitSuccessful
submitSuccessful(value: any)
Parameters :
Name Type Optional
value any No
Returns : void

Properties

Protected _formDefinition
Type : FormDefinition<T>
Public Readonly cdr
Type : ChangeDetectorRef
Decorators :
@Inject(ChangeDetectorRef)
Public Readonly context
Default value : input<Record<string, unknown>>({})
Public form
Type : RxapFormGroup<T>
Public Readonly loaded$
Default value : new ToggleSubject()
Public Readonly loading$
Default value : new ToggleSubject()
Public Readonly loadingError$
Default value : new BehaviorSubject<Error | null>(null)
Public Readonly submitError$
Default value : new BehaviorSubject<Error | null>(null)
Public Readonly submitting$
Default value : new ToggleSubject()

Accessors

submitting
getsubmitting()
submitError
getsubmitError()
loading
getloading()
loaded
getloaded()
loadingError
getloadingError()
useFormDefinition
setuseFormDefinition(value: FormDefinition<T> | string)
Parameters :
Name Type Optional
value FormDefinition<T> | string No
Returns : void
formDefinition
getformDefinition()
import {
  ChangeDetectorRef,
  Directive,
  EventEmitter,
  forwardRef,
  HostBinding,
  Inject,
  input,
  Input,
  isDevMode,
  OnChanges,
  OnDestroy,
  OnInit,
  Optional,
  Output,
  SimpleChanges,
  SkipSelf,
} from '@angular/core';
import {
  ControlContainer,
  FormGroupDirective,
} from '@angular/forms';
import { ToggleSubject } from '@rxap/rxjs';
import { LoadingIndicatorService } from '@rxap/services';
import {
  clone,
  isObject,
  isPromise,
} from '@rxap/utilities';
import {
  BehaviorSubject,
  Subscription,
} from 'rxjs';
import {
  debounceTime,
  filter,
  tap,
} from 'rxjs/operators';
import { RxapFormBuilder } from '../form-builder';
import { RxapFormGroup } from '../form-group';
import { FormDefinition } from '../model';
import {
  FormLoadFailedMethod,
  FormLoadMethod,
  FormLoadSuccessfulMethod,
  FormSubmitFailedMethod,
  FormSubmitMethod,
  FormSubmitSuccessfulMethod,
} from './models';
import {
  RXAP_FORM_DEFINITION,
  RXAP_FORM_DEFINITION_BUILDER,
  RXAP_FORM_LOAD_FAILED_METHOD,
  RXAP_FORM_LOAD_METHOD,
  RXAP_FORM_LOAD_SUCCESSFUL_METHOD,
  RXAP_FORM_SUBMIT_FAILED_METHOD,
  RXAP_FORM_SUBMIT_METHOD,
  RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD,
} from './tokens';

@Directive({
  selector: 'form[rxapForm]:not([formGroup]):not([ngForm]),rxap-form,form[rxapForm]',
  providers: [
    {
      provide: ControlContainer,
      // ignore coverage
      useExisting: forwardRef(() => FormDirective),
    },
    // region form provider clear
    // form provider that are directly associated with the current form
    // are cleared to prevent that inner forms can access this providers
    // Example: The parent form has a submit method provider and the inner should
    // not have a submit method provider. If the parent submit method provider is
    // not cleared then the inner form uses the parent submit method provider on
    // submit
    {
      provide: RXAP_FORM_SUBMIT_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_LOAD_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_LOAD_FAILED_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_LOAD_SUCCESSFUL_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_SUBMIT_FAILED_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD,
      useValue: null,
    },
    {
      provide: RXAP_FORM_DEFINITION_BUILDER,
      useValue: null,
    },
    // endregion
  ],

  host: { '(reset)': 'onReset()' },
  // eslint-disable-next-line @angular-eslint/no-outputs-metadata-property
  outputs: [ 'ngSubmit' ],
  exportAs: 'rxapForm',
  standalone: true,
})
export class FormDirective<T = any>
  extends FormGroupDirective
  implements OnInit, OnChanges, OnDestroy {
  public override form!: RxapFormGroup<T>;

  @Input()
  public initial?: T;

  public readonly context = input<Record<string, unknown>>({});

  /**
   * Emits when the submit method is executed without errors. The result of the
   * submit method is passed as event object.
   *
   * If no submit method is defined then emit after the submit button
   * is clicked.
   */
  @Output()
  public rxapSubmit = new EventEmitter();

  @Output()
  public invalidSubmit = new EventEmitter<Record<string, any>>();

  // eslint-disable-next-line @angular-eslint/no-output-rename
  @Output('submitSuccessful')
  public submitSuccessful$ = new EventEmitter();

  @HostBinding('class.rxap-submitting')
  public get submitting(): boolean {
    return this.submitting$.value;
  }

  @HostBinding('class.rxap-submit-error')
  public get submitError(): Error | null {
    return this.submitError$.value;
  }

  @HostBinding('class.rxap-loading')
  public get loading(): boolean {
    return this.loading$.value;
  }

  @HostBinding('class.rxap-loaded')
  public get loaded(): boolean {
    return this.loaded$.value;
  }

  @HostBinding('class.rxap-loading-error')
  public get loadingError(): Error | null {
    return this.loadingError$.value;
  }

  @Input('rxapForm')
  public set useFormDefinition(value: FormDefinition<T> | '') {
    if (value) {
      this._formDefinition = value as any;
      this.form = value.rxapFormGroup;
    }
  }

  public get formDefinition(): FormDefinition<T> {
    return this._formDefinition;
  }

  public readonly submitting$ = new ToggleSubject();
  public readonly submitError$ = new BehaviorSubject<Error | null>(null);
  public readonly loading$ = new ToggleSubject();
  public readonly loaded$ = new ToggleSubject();
  public readonly loadingError$ = new BehaviorSubject<Error | null>(null);

  protected _formDefinition!: FormDefinition<T>;

  @Input()
  public submitMethod: FormSubmitMethod<any> | null = null;

  @Input()
  public loadMethod: FormLoadMethod | null = null;

  @Input()
  public loadFailedMethod: FormLoadFailedMethod | null = null;

  @Input()
  public loadSuccessfulMethod: FormLoadSuccessfulMethod | null = null;

  @Input()
  public submitFailedMethod: FormSubmitFailedMethod | null = null;

  @Input()
  public submitSuccessfulMethod: FormSubmitSuccessfulMethod | null = null;

  private _autoSubmitSubscription = new Subscription();

  constructor(
    @Inject(ChangeDetectorRef) public readonly cdr: ChangeDetectorRef,
    @Optional()
    @Inject(RXAP_FORM_DEFINITION)
    formDefinition: FormDefinition | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_SUBMIT_METHOD)
    submitMethod: FormSubmitMethod<any> | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_LOAD_METHOD)
    loadMethod: FormLoadMethod | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_LOAD_FAILED_METHOD)
    loadFailedMethod: FormLoadFailedMethod | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_LOAD_SUCCESSFUL_METHOD)
    loadSuccessfulMethod: FormLoadSuccessfulMethod | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_SUBMIT_FAILED_METHOD)
    submitFailedMethod: FormSubmitFailedMethod | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD)
    submitSuccessfulMethod: FormSubmitSuccessfulMethod | null = null,
    // skip self, bc the token is set to null
    @SkipSelf()
    @Optional()
    @Inject(RXAP_FORM_DEFINITION_BUILDER)
    protected readonly formDefinitionBuilder: RxapFormBuilder | null = null,
    @Optional()
    @Inject(LoadingIndicatorService)
    protected readonly loadingIndicatorService: LoadingIndicatorService | null = null,
  ) {
    super([], []);
    this.submitMethod = submitMethod ?? this.submitMethod;
    this.loadMethod = loadMethod ?? this.loadMethod;
    this.loadFailedMethod = loadFailedMethod ?? this.loadFailedMethod;
    this.loadSuccessfulMethod = loadSuccessfulMethod ?? this.loadSuccessfulMethod;
    this.submitFailedMethod = submitFailedMethod ?? this.submitFailedMethod;
    this.submitSuccessfulMethod = submitSuccessfulMethod ?? this.submitSuccessfulMethod;
    if (!formDefinition && formDefinitionBuilder) {
      formDefinition = formDefinitionBuilder.build<FormDefinition>();
    }
    if (formDefinition) {
      this._formDefinition = formDefinition;
      this.form = formDefinition.rxapFormGroup;
    }
    this.loadingIndicatorService?.attachLoading(this.loading$);
    this.loadingIndicatorService?.attachLoading(this.submitting$);
  }

  public override ngOnChanges(changes: SimpleChanges) {
    super.ngOnChanges(changes);

    const formChange = changes['form'];

    if (formChange && !formChange.firstChange) {
      this.loadInitialState(formChange.currentValue);
    }
  }

  public ngOnInit() {
    if (!this._formDefinition) {
      // TODO : replace with rxap error
      throw new Error('The form definition instance is not defined');
    }
    if (!this.form) {
      // TODO : replace with rxap error
      throw new Error('The form instance is not defined');
    }
    this.loadInitialState(this.form);

    function HasNgOnInitMethod(obj: any): obj is OnInit {
      return obj && typeof obj.ngOnInit === 'function';
    }

    if (HasNgOnInitMethod(this._formDefinition)) {
      this._formDefinition.ngOnInit();
    }

    if (this._formDefinition.rxapMetadata.autoSubmit) {
      const debounce =
        typeof this._formDefinition.rxapMetadata.autoSubmit === 'number'
          ? this._formDefinition.rxapMetadata.autoSubmit
          : 5000;
      this._autoSubmitSubscription = this
        .form
        .valueChanges
        .pipe(
          debounceTime(debounce),
          filter(() => this.form.valid),
          tap((value) => {
              if (isDevMode()) {
                console.debug(
                  `Auto submit form '${ this._formDefinition.rxapMetadata.id }'`,
                  value,
                );
              }
            },
          ),
          tap(() => this.submit()),
        )
        .subscribe();
    }
  }

  public override onSubmit($event: Event): boolean {
    $event.preventDefault();
    super.onSubmit($event);
    if (this.form.valid) {
      this.submit();
    } else {

      // eslint-disable-next-line no-inner-declarations
      function reduceErrors(control: any, errors: Record<string, any> = {}): Record<string, any> {

        if (control.invalid) {
          if (control.controls) {
            if (Array.isArray(control.controls)) {
              const errorList = [];
              for (const item of control.controls) {
                errorList.push(reduceErrors(item));
              }
              errors[control.controlId] = errorList;
            } else {
              const innerErrors = {};
              for (const child of Object.values(control.controls)) {
                reduceErrors(child, innerErrors);
              }
              errors[control.controlId] = innerErrors;
            }
          } else {
            if (control.errors) {
              errors[control.controlId] = control.errors;
            }
          }
        }

        return errors;
      }

      if (isDevMode()) {
        console.log(
          'Form submit is not valid for: ' + this.form.controlId,
          this.form.errors,
        );

        // eslint-disable-next-line no-inner-declarations
        function printErrorControls(control: any) {
          if (!control.valid) {
            console.group(control.controlId);
            if (control.controls) {
              if (Array.isArray(control.controls)) {
                for (let i = 0; i < control.controls.length; i++) {
                  const child = control.controls[i];
                  if (!child.valid) {
                    console.group(`index: ${ i }`);
                    printErrorControls(child);
                    console.groupEnd();
                  }
                }
              } else {
                for (const child of Object.values(control.controls)) {
                  printErrorControls(child);
                }
              }
            } else {
              if (control.errors) {
                for (const [ key, value ] of Object.entries(control.errors)) {
                  console.group(key);
                  console.log(value);
                  console.groupEnd();
                }
              }
              console.log('value: ', control.value);
            }
            console.groupEnd();
          }
        }

        printErrorControls(this.form);
      }
      this.invalidSubmit.emit(reduceErrors(this.form));
    }

    return false;
  }

  protected loadInitialState(form: RxapFormGroup): void {
    if (this.initial) {
      if (isDevMode()) {
        console.log('use the value from input initial');
      }
      form.patchValue(this.initial);
    } else {
      if (this.loadMethod) {
        this.loading$.enable();

        try {
          const resultOrPromise = this.loadMethod.call();
          if (isPromise(resultOrPromise)) {
            resultOrPromise
              .then((value) => {
                form.patchValue(value);
                this.loaded$.enable();
                this.loadSuccessful(value);
              })
              .catch((error) => {
                this.loadingError$.next(error);
                this.loadFailed(error);
              })
              .finally(() => {
                this.loading$.disable();
                this.cdr.detectChanges();
              });
          } else if (isObject(resultOrPromise)) {
            form.patchValue(resultOrPromise);
            this.loaded$.enable();
            this.loadSuccessful(resultOrPromise);
            this.loading$.disable();
          }
        } catch (error: any) {
          this.loaded$.disable();
          this.loadingError$.next(error);
          this.loadFailed(error);
          this.loading$.disable();
        }
      } else {
        if (isDevMode()) {
          console.warn(
            'The form loading method is not defined for: ' + this.form.controlId,
          );
        }
        this.loaded$.enable();
      }
    }
  }

  protected loadSuccessful(value: any) {
    if (this.loadSuccessfulMethod) {
      this.loadSuccessfulMethod.call(value);
    } else if (isDevMode()) {
      console.warn(
        'The load successful is not defined for: ' + this.form.controlId,
      );
    }
  }

  protected loadFailed(error: Error) {
    console.debug('Load Error:', error);
    console.error('Load Error:', error.message);
    if (this.loadFailedMethod) {
      this.loadFailedMethod?.call(error);
    } else if (isDevMode()) {
      console.warn('The form loading failed for: ' + this.form.controlId);
    }
  }

  protected getSubmitValue(): T {
    let value: T = undefined as any;

    if (typeof this._formDefinition['getSubmitValue'] === 'function') {
      value = this._formDefinition.getSubmitValue();
    } else if (typeof this._formDefinition['toJSON'] === 'function') {
      value = this._formDefinition.toJSON();
    }

    value = value ?? this.form.value;

    return clone(value);
  }

  protected submit() {
    const value = this.getSubmitValue();
    if (this.submitMethod) {
      Reflect.set(this, 'submitted', false);
      this.submitting$.enable();
      this.submitError$.next(null);
      try {
        const resultOrPromise = this.submitMethod.call(value, this.context());
        if (isPromise(resultOrPromise)) {
          resultOrPromise
            .then((result) => {
              Reflect.set(this, 'submitted', true);
              this.rxapSubmit.emit(result);
              this.submitSuccessful(result);
            })
            .catch((error) => {
              this.submitError$.next(error);
              this.submitFailed(error);
            })
            .finally(() => {
              this.submitting$.disable();
              this.cdr.detectChanges();
            });
        } else {
          Reflect.set(this, 'submitted', true);
          this.rxapSubmit.emit(resultOrPromise);
          this.submitSuccessful(resultOrPromise);
          this.submitting$.disable();
        }
      } catch (error: any) {
        this.submitting$.disable();
        this.submitError$.next(error);
        this.submitFailed(error);
      }
    } else {
      if (isDevMode()) {
        console.warn(
          'The form submit method is not defined for: ' + this.form.controlId,
        );
      }

      this.rxapSubmit.emit(value);
      this.submitSuccessful(value);
    }
  }

  protected submitFailed(error: Error) {
    console.debug('Submit Error:', error);
    console.error('Submit Error:', error.message);
    if (this.submitFailedMethod) {
      this.submitFailedMethod.call(error);
    } else if (isDevMode()) {
      console.warn(
        'The form submit failed method is not defined for: ' +
        this.form.controlId,
      );
    }
  }

  protected submitSuccessful(value: any) {
    this.submitSuccessful$.next(value);
    if (this.submitSuccessfulMethod) {
      this.submitSuccessfulMethod.call(value);
    } else if (isDevMode()) {
      console.warn(
        'The form submit successful method is not defined for: ' +
        this.form.controlId,
      );
    }
  }

  public override ngOnDestroy() {
    super.ngOnDestroy();
    this._autoSubmitSubscription?.unsubscribe();

    function HasNgOnDestroyMethod(obj: any): obj is OnDestroy {
      return obj && typeof obj.ngOnDestroy === 'function';
    }

    if (HasNgOnDestroyMethod(this._formDefinition)) {
      this._formDefinition.ngOnDestroy();
    }

  }
}

results matching ""

    No results matching ""