Fixed server side form validation

This commit is contained in:
Giuseppe Digilio
2018-07-09 19:02:09 +02:00
parent 499b5bf51b
commit 4d89674cda
8 changed files with 95 additions and 36 deletions

View File

@@ -22,6 +22,14 @@ module.exports = {
// msToLive: 1000, // 15 minutes // msToLive: 1000, // 15 minutes
control: 'max-age=60' // revalidate browser 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
notifications: { notifications: {
rtl: false, rtl: false,

View File

@@ -22,6 +22,7 @@ import { FormState } from './form.reducer';
import { FormChangeAction, FormStatusChangeAction } from './form.actions'; import { FormChangeAction, FormStatusChangeAction } from './form.actions';
import { MockStore } from '../testing/mock-store'; import { MockStore } from '../testing/mock-store';
import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model';
import { GLOBAL_CONFIG } from '../../../config';
function createTestComponent<T>(html: string, type: { new(...args: any[]): T }): ComponentFixture<T> { function createTestComponent<T>(html: string, type: { new(...args: any[]): T }): ComponentFixture<T> {
TestBed.overrideComponent(type, { TestBed.overrideComponent(type, {
@@ -102,11 +103,19 @@ export const TEST_FORM_MODEL_WITH_ARRAY = [
]; ];
describe('FormComponent test suite', () => { describe('FormComponent test suite', () => {
let testComp: TestComponent; let testComp: TestComponent;
let formComp: FormComponent; let formComp: FormComponent;
let testFixture: ComponentFixture<TestComponent>; let testFixture: ComponentFixture<TestComponent>;
let formFixture: ComponentFixture<FormComponent>; let formFixture: ComponentFixture<FormComponent>;
const config = {
form: {
validatorMap: {
required: 'required',
regex: 'pattern'
}
}
} as any;
const formState: FormState = { const formState: FormState = {
testForm: { testForm: {
data: { data: {
@@ -146,6 +155,7 @@ describe('FormComponent test suite', () => {
FormBuilderService, FormBuilderService,
FormComponent, FormComponent,
FormService, FormService,
{provide: GLOBAL_CONFIG, useValue: config},
{ {
provide: Store, useValue: store provide: Store, useValue: store
} }

View File

@@ -13,8 +13,17 @@ import { FormService } from './form.service';
import { FormBuilderService } from './builder/form-builder.service'; import { FormBuilderService } from './builder/form-builder.service';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { formReducer } from './form.reducer'; import { formReducer } from './form.reducer';
import { GlobalConfig } from '../../../config/global-config.interface';
describe('FormService test suite', () => { describe('FormService test suite', () => {
const config = {
form: {
validatorMap: {
required: 'required',
regex: 'pattern'
}
}
} as any;
const formId = 'testForm'; const formId = 'testForm';
let service: FormService; let service: FormService;
let builderService: FormBuilderService; let builderService: FormBuilderService;
@@ -98,7 +107,7 @@ describe('FormService test suite', () => {
}); });
builderService = formBuilderService; builderService = formBuilderService;
formGroup = builderService.createFormGroup(formModel); formGroup = builderService.createFormGroup(formModel);
service = new FormService(formBuilderService, store); service = new FormService(config, formBuilderService, store);
})); }));
it('should check whether form state is init', () => { it('should check whether form state is init', () => {

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -6,15 +6,18 @@ import { Store } from '@ngrx/store';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { formObjectFromIdSelector } from './selectors'; import { formObjectFromIdSelector } from './selectors';
import { FormBuilderService } from './builder/form-builder.service'; import { FormBuilderService } from './builder/form-builder.service';
import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core';
import { isEmpty, isNotEmpty, isNotUndefined } from '../empty.util'; import { isEmpty, isNotUndefined } from '../empty.util';
import { find, uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { FormChangeAction, FormRemoveErrorAction } from './form.actions'; import { FormChangeAction } from './form.actions';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable() @Injectable()
export class FormService { export class FormService {
constructor(private formBuilderService: FormBuilderService, constructor(
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
private formBuilderService: FormBuilderService,
private store: Store<AppState>) { private store: Store<AppState>) {
} }
@@ -38,6 +41,16 @@ export class FormService {
.distinctUntilChanged(); .distinctUntilChanged();
} }
/**
* Method to retrieve form's errors from state
*/
public getFormErrors(formId: string): Observable<any> {
return this.store.select(formObjectFromIdSelector(formId))
.filter((state) => isNotUndefined(state))
.map((state) => state.errors)
.distinctUntilChanged();
}
/** /**
* Method to retrieve form's data from state * Method to retrieve form's data from state
*/ */
@@ -66,50 +79,42 @@ export class FormService {
} }
public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) { public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) {
const error = {}; // create the error object 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 form control model has not errorMessages object, create it
if (!model.errorMessages) { if (!model.errorMessages) {
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 // check if error code is already present in the set of model's validators
const last = lastArray[lastArray.length - 1]; if (isEmpty(model.errorMessages[errorKey])) {
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 // put the error message in the form control model
model.errorMessages[errorKey] = message; model.errorMessages[errorKey] = message;
} else { } else {
error[last] = modelMsg; // 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 // add the error in the form control
field.setErrors(error); field.setErrors(error);
}
field.markAsTouched(); field.markAsTouched();
} }
public removeErrorFromField(field: AbstractControl, model: DynamicFormControlModel, messageKey: string) { public removeErrorFromField(field: AbstractControl, model: DynamicFormControlModel, messageKey: string) {
const error = {}; const error = {};
const errorKey = this.getValidatorNameFromMap(messageKey);
if (messageKey.includes('.')) { if (field.hasError(errorKey)) {
// Use correct error messages from the model error[errorKey] = null;
const lastArray = messageKey.split('.');
if (lastArray && lastArray.length > 0) {
const last = lastArray[lastArray.length - 1];
error[last] = null;
}
} else {
error[messageKey] = null;
}
field.setErrors(error); field.setErrors(error);
}
field.markAsUntouched(); field.markAsUntouched();
} }
@@ -118,4 +123,14 @@ export class FormService {
formGroup.reset(); formGroup.reset();
this.store.dispatch(new FormChangeAction(formId, formGroup.value)); 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;
}
} }

View File

@@ -16,6 +16,7 @@
placeholder="{{placeholder}}" placeholder="{{placeholder}}"
[name]="name" [name]="name"
[(ngModel)]="value" [(ngModel)]="value"
(blur)="onBlur($event); $event.stopPropagation();"
(change)="update($event); $event.stopPropagation();" (change)="update($event); $event.stopPropagation();"
(focus)="onFocus($event); $event.stopPropagation();" (focus)="onFocus($event); $event.stopPropagation();"
[readonly]="disabled" [readonly]="disabled"

View File

@@ -25,6 +25,7 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor {
@Output() selected = new EventEmitter<number>(); @Output() selected = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>(); @Output() remove = new EventEmitter<number>();
@Output() blur = new EventEmitter<any>();
@Output() change = new EventEmitter<any>(); @Output() change = new EventEmitter<any>();
@Output() focus = new EventEmitter<any>(); @Output() focus = new EventEmitter<any>();
@@ -114,6 +115,10 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor {
} }
} }
onBlur(event) {
this.blur.emit(event);
}
onFocus(event) { onFocus(event) {
if (this.value) { if (this.value) {
this.startValue = this.value; this.startValue = this.value;

View File

@@ -0,0 +1,9 @@
import { Config } from './config.interface';
export interface ValidatorMap {
[validator: string]: string;
}
export interface FormConfig extends Config {
validatorMap: ValidatorMap;
}

View File

@@ -3,12 +3,14 @@ import { ServerConfig } from './server-config.interface';
import { CacheConfig } from './cache-config.interface'; import { CacheConfig } from './cache-config.interface';
import { UniversalConfig } from './universal-config.interface'; import { UniversalConfig } from './universal-config.interface';
import { INotificationBoardOptions } from './notifications-config.interfaces'; import { INotificationBoardOptions } from './notifications-config.interfaces';
import { FormConfig } from './form-config.interfaces';
export interface GlobalConfig extends Config { export interface GlobalConfig extends Config {
ui: ServerConfig; ui: ServerConfig;
rest: ServerConfig; rest: ServerConfig;
production: boolean; production: boolean;
cache: CacheConfig; cache: CacheConfig;
form: FormConfig;
notifications: INotificationBoardOptions; notifications: INotificationBoardOptions;
universal: UniversalConfig; universal: UniversalConfig;
gaTrackingId: string; gaTrackingId: string;