mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 04:23:04 +00:00
Fixed server side form validation
This commit is contained in:
@@ -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,
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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', () => {
|
||||||
|
@@ -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,16 +6,19 @@ 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(
|
||||||
private store: Store<AppState>) {
|
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
|
private formBuilderService: FormBuilderService,
|
||||||
|
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
|
// check if error code is already present in the set of model's validators
|
||||||
const lastArray = message.split('.');
|
if (isEmpty(model.errorMessages[errorKey])) {
|
||||||
if (lastArray && lastArray.length > 0) {
|
// put the error message in the form control model
|
||||||
// check if error code is already present in the set of model's validators
|
model.errorMessages[errorKey] = message;
|
||||||
const last = lastArray[lastArray.length - 1];
|
} else {
|
||||||
const modelMsg = model.errorMessages[last];
|
// Use correct error messages from the model
|
||||||
if (isEmpty(modelMsg)) {
|
errorMsg = model.errorMessages[errorKey];
|
||||||
const errorKey = uniqueId('error-'); // create a single key for the error
|
}
|
||||||
error[errorKey] = true;
|
|
||||||
// put the error message in the form control model
|
if (!field.hasError(errorKey)) {
|
||||||
model.errorMessages[errorKey] = message;
|
error[errorKey] = true;
|
||||||
} else {
|
// add the error in the form control
|
||||||
error[last] = modelMsg;
|
field.setErrors(error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the error in the form control
|
|
||||||
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('.');
|
field.setErrors(error);
|
||||||
if (lastArray && lastArray.length > 0) {
|
|
||||||
const last = lastArray[lastArray.length - 1];
|
|
||||||
error[last] = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error[messageKey] = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
||||||
|
@@ -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;
|
||||||
|
9
src/config/form-config.interfaces.ts
Normal file
9
src/config/form-config.interfaces.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
export interface ValidatorMap {
|
||||||
|
[validator: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormConfig extends Config {
|
||||||
|
validatorMap: ValidatorMap;
|
||||||
|
}
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user