diff --git a/config/environment.default.js b/config/environment.default.js index 4f3aee5f0e..a6ef738f41 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -22,6 +22,14 @@ module.exports = { // msToLive: 1000, // 15 minutes control: 'max-age=60' // revalidate browser }, + // Form settings + form: { + // NOTE: Map server-side validators to comparative Angular form validators + validatorMap: { + required: 'required', + regex: 'pattern' + } + }, // Notifications notifications: { rtl: false, diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index ccffe920d1..576af76141 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -22,6 +22,7 @@ import { FormState } from './form.reducer'; import { FormChangeAction, FormStatusChangeAction } from './form.actions'; import { MockStore } from '../testing/mock-store'; import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model'; +import { GLOBAL_CONFIG } from '../../../config'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -102,11 +103,19 @@ export const TEST_FORM_MODEL_WITH_ARRAY = [ ]; describe('FormComponent test suite', () => { - let testComp: TestComponent; let formComp: FormComponent; let testFixture: ComponentFixture; let formFixture: ComponentFixture; + + const config = { + form: { + validatorMap: { + required: 'required', + regex: 'pattern' + } + } + } as any; const formState: FormState = { testForm: { data: { @@ -146,6 +155,7 @@ describe('FormComponent test suite', () => { FormBuilderService, FormComponent, FormService, + {provide: GLOBAL_CONFIG, useValue: config}, { provide: Store, useValue: store } diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index dda6e018c6..06125c9034 100644 --- a/src/app/shared/form/form.service.spec.ts +++ b/src/app/shared/form/form.service.spec.ts @@ -13,8 +13,17 @@ import { FormService } from './form.service'; import { FormBuilderService } from './builder/form-builder.service'; import { AppState } from '../../app.reducer'; import { formReducer } from './form.reducer'; +import { GlobalConfig } from '../../../config/global-config.interface'; describe('FormService test suite', () => { + const config = { + form: { + validatorMap: { + required: 'required', + regex: 'pattern' + } + } + } as any; const formId = 'testForm'; let service: FormService; let builderService: FormBuilderService; @@ -98,7 +107,7 @@ describe('FormService test suite', () => { }); builderService = formBuilderService; formGroup = builderService.createFormGroup(formModel); - service = new FormService(formBuilderService, store); + service = new FormService(config, formBuilderService, store); })); it('should check whether form state is init', () => { diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 823b00d69b..d3f6c984ba 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs/Observable'; import { Store } from '@ngrx/store'; @@ -6,16 +6,19 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; -import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; -import { isEmpty, isNotEmpty, isNotUndefined } from '../empty.util'; -import { find, uniqueId } from 'lodash'; -import { FormChangeAction, FormRemoveErrorAction } from './form.actions'; +import { DynamicFormControlModel } from '@ng-dynamic-forms/core'; +import { isEmpty, isNotUndefined } from '../empty.util'; +import { uniqueId } from 'lodash'; +import { FormChangeAction } from './form.actions'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; @Injectable() export class FormService { - constructor(private formBuilderService: FormBuilderService, - private store: Store) { + constructor( + @Inject(GLOBAL_CONFIG) public config: GlobalConfig, + private formBuilderService: FormBuilderService, + private store: Store) { } /** @@ -38,6 +41,16 @@ export class FormService { .distinctUntilChanged(); } + /** + * Method to retrieve form's errors from state + */ + public getFormErrors(formId: string): Observable { + return this.store.select(formObjectFromIdSelector(formId)) + .filter((state) => isNotUndefined(state)) + .map((state) => state.errors) + .distinctUntilChanged(); + } + /** * Method to retrieve form's data from state */ @@ -66,50 +79,42 @@ export class FormService { } 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 not errorMessages object, create it if (!model.errorMessages) { model.errorMessages = {}; } - // Use correct error messages from the model - const lastArray = message.split('.'); - if (lastArray && lastArray.length > 0) { - // check if error code is already present in the set of model's validators - const last = lastArray[lastArray.length - 1]; - const modelMsg = model.errorMessages[last]; - if (isEmpty(modelMsg)) { - const errorKey = uniqueId('error-'); // create a single key for the error - error[errorKey] = true; - // put the error message in the form control model - model.errorMessages[errorKey] = message; - } else { - error[last] = modelMsg; - } + // 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); } - // add the error in the form control - field.setErrors(error); field.markAsTouched(); } public removeErrorFromField(field: AbstractControl, model: DynamicFormControlModel, messageKey: string) { const error = {}; + const errorKey = this.getValidatorNameFromMap(messageKey); - if (messageKey.includes('.')) { - // Use correct error messages from the model - const lastArray = messageKey.split('.'); - if (lastArray && lastArray.length > 0) { - const last = lastArray[lastArray.length - 1]; - error[last] = null; - } - } else { - error[messageKey] = null; + if (field.hasError(errorKey)) { + error[errorKey] = null; + field.setErrors(error); } - field.setErrors(error); field.markAsUntouched(); } @@ -118,4 +123,14 @@ export class FormService { 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 (this.config.form.validatorMap.hasOwnProperty(validator)) ? this.config.form.validatorMap[validator] : validator; + } } diff --git a/src/app/shared/number-picker/number-picker.component.html b/src/app/shared/number-picker/number-picker.component.html index e1e559c535..e2007f0f28 100644 --- a/src/app/shared/number-picker/number-picker.component.html +++ b/src/app/shared/number-picker/number-picker.component.html @@ -16,6 +16,7 @@ placeholder="{{placeholder}}" [name]="name" [(ngModel)]="value" + (blur)="onBlur($event); $event.stopPropagation();" (change)="update($event); $event.stopPropagation();" (focus)="onFocus($event); $event.stopPropagation();" [readonly]="disabled" diff --git a/src/app/shared/number-picker/number-picker.component.ts b/src/app/shared/number-picker/number-picker.component.ts index 99cc138d8c..0bc47e9438 100644 --- a/src/app/shared/number-picker/number-picker.component.ts +++ b/src/app/shared/number-picker/number-picker.component.ts @@ -25,6 +25,7 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { @Output() selected = new EventEmitter(); @Output() remove = new EventEmitter(); + @Output() blur = new EventEmitter(); @Output() change = new EventEmitter(); @Output() focus = new EventEmitter(); @@ -114,6 +115,10 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { } } + onBlur(event) { + this.blur.emit(event); + } + onFocus(event) { if (this.value) { this.startValue = this.value; diff --git a/src/config/form-config.interfaces.ts b/src/config/form-config.interfaces.ts new file mode 100644 index 0000000000..e13fdc1c60 --- /dev/null +++ b/src/config/form-config.interfaces.ts @@ -0,0 +1,9 @@ +import { Config } from './config.interface'; + +export interface ValidatorMap { + [validator: string]: string; +} + +export interface FormConfig extends Config { + validatorMap: ValidatorMap; +} diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index 7c05b78fa5..b623a4bf8c 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -3,12 +3,14 @@ import { ServerConfig } from './server-config.interface'; import { CacheConfig } from './cache-config.interface'; import { UniversalConfig } from './universal-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; +import { FormConfig } from './form-config.interfaces'; export interface GlobalConfig extends Config { ui: ServerConfig; rest: ServerConfig; production: boolean; cache: CacheConfig; + form: FormConfig; notifications: INotificationBoardOptions; universal: UniversalConfig; gaTrackingId: string;