import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; import { DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; import { isEmpty, isNotUndefined } from '../empty.util'; import uniqueId from 'lodash/uniqueId'; import { FormAddError, FormAddTouchedAction, FormChangeAction, FormInitAction, FormRemoveAction, FormRemoveErrorAction, FormStatusChangeAction } from './form.actions'; import { FormEntry, FormError, FormTouchedState } from './form.reducer'; import { environment } from '../../../environments/environment'; @Injectable() export class FormService { constructor( private formBuilderService: FormBuilderService, private store: Store) { } /** * Method to retrieve form's status from state */ public isValid(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), filter((state) => isNotUndefined(state)), map((state) => state.valid), distinctUntilChanged() ); } /** * Method to retrieve form's data from state */ public getFormData(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), filter((state) => isNotUndefined(state)), map((state) => state.data), distinctUntilChanged() ); } /** * Method to retrieve form's touched state */ public getFormTouchedState(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), filter((state) => isNotUndefined(state)), map((state) => state.touched), distinctUntilChanged() ); } /** * Method to retrieve form's errors from state */ public getFormErrors(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), filter((state) => isNotUndefined(state)), map((state) => state.errors), distinctUntilChanged() ); } /** * Method to retrieve form's data from state */ public isFormInitialized(formId: string): Observable { return this.store.pipe( select(formObjectFromIdSelector(formId)), distinctUntilChanged(), map((state) => isNotUndefined(state)) ); } public getUniqueId(formId): string { return uniqueId() + '_' + formId; } /** * Method to validate form's fields */ public validateAllFormFields(formGroup: UntypedFormGroup | UntypedFormArray) { Object.keys(formGroup.controls).forEach((field) => { const control = formGroup.get(field); if (control instanceof UntypedFormControl) { control.markAsTouched({ onlySelf: true }); control.markAsDirty({ onlySelf: true }); } else if (control instanceof UntypedFormGroup || control instanceof UntypedFormArray) { this.validateAllFormFields(control); } }); } /** * Check if form group has an invalid form control * @param formGroup The form group to check */ public hasValidationErrors(formGroup: UntypedFormGroup | UntypedFormArray): boolean { let hasErrors = false; const fields: string[] = Object.keys(formGroup.controls); for (const field of fields) { const control = formGroup.get(field); if (control instanceof UntypedFormControl) { hasErrors = !control.valid && control.touched; } else if (control instanceof UntypedFormGroup || control instanceof UntypedFormArray) { hasErrors = this.hasValidationErrors(control); } if (hasErrors) { break; } } return hasErrors; } public addControlErrors(field: AbstractControl, formId: string, fieldId: string, fieldIndex: number) { if (field.errors === null) { return; } const errors: string[] = Object.keys(field.errors) .filter((errorKey) => field.errors[errorKey] === true) .map((errorKey) => `error.validation.${errorKey}`); errors.forEach((error) => this.addError(formId, fieldId, fieldIndex, error)); } public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) { const error = {}; // create the error object const errorKey = this.getValidatorNameFromMap(message); let errorMsg = message; // if form control model has no errorMessages object, create it if (!model.errorMessages) { model.errorMessages = {}; } // check if error code is already present in the set of model's validators if (isEmpty(model.errorMessages[errorKey])) { // put the error message in the form control model model.errorMessages[errorKey] = message; } else { // Use correct error messages from the model errorMsg = model.errorMessages[errorKey]; } if (!field.hasError(errorKey)) { error[errorKey] = true; // add the error in the form control field.setErrors(error); } // if the field in question is a concat group, pass down the error to its fields if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; this.addErrorToField(subField, subModel, message); }); } field.markAsTouched(); } public removeErrorFromField(field: AbstractControl, model: DynamicFormControlModel, messageKey: string) { const error = {}; const errorKey = this.getValidatorNameFromMap(messageKey); if (field.hasError(errorKey)) { error[errorKey] = null; field.setErrors(error); } // if the field in question is a concat group, clear the error from its fields if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; this.removeErrorFromField(subField, subModel, messageKey); }); } field.markAsUntouched(); } public resetForm(formGroup: UntypedFormGroup, groupModel: DynamicFormControlModel[], formId: string) { this.formBuilderService.clearAllModelsValue(groupModel); formGroup.reset(); this.store.dispatch(new FormChangeAction(formId, formGroup.value)); } private getValidatorNameFromMap(validator): string { if (validator.includes('.')) { const splitArray = validator.split('.'); if (splitArray && splitArray.length > 0) { validator = this.getValidatorNameFromMap(splitArray[splitArray.length - 1]); } } return (environment.form.validatorMap.hasOwnProperty(validator)) ? environment.form.validatorMap[validator] : validator; } public initForm(formId: string, model: DynamicFormControlModel[], valid: boolean) { this.store.dispatch(new FormInitAction(formId, this.formBuilderService.getValueFromModel(model), valid)); } public setStatusChanged(formId: string, valid: boolean) { this.store.dispatch(new FormStatusChangeAction(formId, valid)); } public getForm(formId: string): Observable { return this.store.pipe(select(formObjectFromIdSelector(formId))); } public removeForm(formId: string) { this.store.dispatch(new FormRemoveAction(formId)); } public changeForm(formId: string, model: DynamicFormControlModel[]) { this.store.dispatch(new FormChangeAction(formId, this.formBuilderService.getValueFromModel(model))); } public setTouched(formId: string, model: DynamicFormControlModel[], event: DynamicFormControlEvent) { const ids = this.formBuilderService.getMetadataIdsFromEvent(event); this.store.dispatch(new FormAddTouchedAction(formId, ids)); } public addError(formId: string, fieldId: string, fieldIndex: number, message: string) { const normalizedFieldId = fieldId.replace(/\./g, '_'); this.store.dispatch(new FormAddError(formId, normalizedFieldId, fieldIndex, message)); } public removeError(formId: string, fieldId: string, fieldIndex: number) { const normalizedFieldId = fieldId.replace(/\./g, '_'); this.store.dispatch(new FormRemoveErrorAction(formId, normalizedFieldId, fieldIndex)); } }