Skip to content

Commit

Permalink
editor: add custom validators support
Browse files Browse the repository at this point in the history
* Adds custom validators support for repeatable fields.
* Fixes missing duplicate and trash support for form field in non
  long mode.

Co-Authored-by: Johnny Mariéthoz <Johnny.Mariethoz@rero.ch>
  • Loading branch information
jma committed Feb 23, 2021
1 parent 47cb05c commit e6e1fc6
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 188 deletions.
137 changes: 23 additions & 114 deletions projects/rero/ng-core/src/lib/record/editor/editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
import { TranslateService } from '@ngx-translate/core';
import { JSONSchema7 as JSONSchema7Base } from 'json-schema';
import { cloneDeep } from 'lodash-es';
import moment from 'moment';
import { BsModalService } from 'ngx-bootstrap/modal';
import { NgxSpinnerService } from 'ngx-spinner';
import { ToastrService } from 'ngx-toastr';
Expand Down Expand Up @@ -109,6 +108,14 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy {
// Config for resource
private _resourceConfig: any;

// list of custom validators
private _customValidators = [
'valueAlreadyExists',
'uniqueValueKeysInObject',
'dateMustBeGreaterThan',
'dateMustBeLessThan'
];

/**
* Constructor.
* @param _formlyJsonschema Formly JSON schema.
Expand Down Expand Up @@ -407,6 +414,9 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy {
field.defaultValue = new Array(jsonSchema.minItems);
}
const formOptions = jsonSchema.form;
field.templateOptions.longMode = this.editorSettings.longMode;
field.templateOptions.recordType = this.recordType;
field.templateOptions.pid = this.pid;

if (formOptions) {
this._setSimpleOptions(field, formOptions);
Expand All @@ -415,7 +425,6 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy {
this._setRemoteTypeahead(field, formOptions);
}

field.templateOptions.longMode = this.editorSettings.longMode;

if (this._resourceConfig != null && this._resourceConfig.formFieldMap) {
return this._resourceConfig.formFieldMap(field, jsonSchema);
Expand Down Expand Up @@ -648,6 +657,7 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy {
private _setValidation(field: FormlyFieldConfig, formOptions: any) {
if (formOptions.validation) {
// custom validation messages
// TODO: use widget instead
const messages = formOptions.validation.messages;
if (messages) {
if (!field.validation) {
Expand All @@ -666,122 +676,21 @@ export class EditorComponent implements OnInit, OnChanges, OnDestroy {
this._translateService.stream(msg) as any;
}
}
// custom validators
if (formOptions.validation.validators) {
// asyncValidators: valueAlreadyExists
if (formOptions.validation.validators.valueAlreadyExists) {
const remoteRecordType =
formOptions.validation.validators.valueAlreadyExists.remoteRecordType;
const limitToValues =
formOptions.validation.validators.valueAlreadyExists.limitToValues;
const filter =
formOptions.validation.validators.valueAlreadyExists.filter;
const term = formOptions.validation.validators.valueAlreadyExists.term;
field.asyncValidators = {
validation: [
(control: FormControl) => {
return this._recordService.uniqueValue(
field,
remoteRecordType ? remoteRecordType : this.recordType,
this.pid ? this.pid : null,
term ? term : null,
limitToValues ? limitToValues : [],
filter ? filter : null
);
}
]
};
delete formOptions.validation.validators.valueAlreadyExists;
}
// asyncValidators: valueKeysInObject
// This validator is similar to uniqueValidator but only check on some specific fields of array items.
if (formOptions.validation.validators.uniqueValueKeysInObject) {
field.validators = {
uniqueValueKeysInObject: {
expression: (control: FormControl) => {
// if value isn't an array or array contains less than 2 elements, no need to check
if (!(control.value instanceof Array) || control.value.length < 2) {
return true;
}
const keysToKeep = formOptions.validation.validators.uniqueValueKeysInObject.keys;
const uniqueItems = Array.from(
new Set(control.value.map((v: any) => {
const keys = keysToKeep.reduce((acc, elt) => {
acc[elt] = v[elt];
return acc;
}, {});
return JSON.stringify(keys);
})),
);
return uniqueItems.length === control.value.length;
}
}
};
}

// The start date must be less than the end date.
if (formOptions.validation.validators.dateMustBeLessThan) {
const startDate: string = formOptions.validation.validators.dateMustBeLessThan.startDate;
const endDate: string = formOptions.validation.validators.dateMustBeLessThan.endDate;
const strict: boolean = formOptions.validation.validators.dateMustBeLessThan.strict || false;
const updateOn: 'change' | 'blur' | 'submit' =
formOptions.validation.validators.dateMustBeLessThan.strict || 'blur';
field.validators = {
dateMustBeLessThan: {
updateOn,
expression: (control: FormControl) => {
const startDateFc = control.parent.get(startDate);
const endDateFc = control.parent.get(endDate);
if (startDateFc.value !== null && endDateFc.value !== null) {
const dateStart = moment(startDateFc.value, 'YYYY-MM-DD');
const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD');
const isMustLessThan = strict
? dateStart >= dateEnd ? false : true
: dateStart > dateEnd ? false : true;
if (isMustLessThan) {
endDateFc.setErrors(null);
endDateFc.markAsDirty();
}
return isMustLessThan;
}
return false;
}
}
};
}

// The end date must be greater than the start date.
if (formOptions.validation.validators.dateMustBeGreaterThan) {
const startDate: string = formOptions.validation.validators.dateMustBeGreaterThan.startDate;
const endDate: string = formOptions.validation.validators.dateMustBeGreaterThan.endDate;
const strict: boolean = formOptions.validation.validators.dateMustBeGreaterThan.strict || false;
const updateOn: 'change' | 'blur' | 'submit' =
formOptions.validation.validators.dateMustBeGreaterThan.strict || 'blur';
field.validators = {
datesMustBeGreaterThan: {
updateOn,
expression: (control: FormControl) => {
const startDateFc = control.parent.get(startDate);
const endDateFc = control.parent.get(endDate);
if (startDateFc.value !== null && endDateFc.value !== null) {
const dateStart = moment(startDateFc.value, 'YYYY-MM-DD');
const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD');
const isMustBeGreaterThan = strict
? dateStart <= dateEnd ? true : false
: dateStart < dateEnd ? true : false;
if (isMustBeGreaterThan) {
startDateFc.setErrors(null);
startDateFc.markAsDirty();
}
return isMustBeGreaterThan;
}
return false;
}
}
};
// store the custom validators config
field.templateOptions.customValidators = {};
for (const customValidator of this._customValidators) {
if (formOptions.validation && formOptions.validation.validators) {
const validatorConfig = formOptions.validation.validators[customValidator];
if (validatorConfig != null) {
field.templateOptions.customValidators[customValidator] = validatorConfig;
}
}
}

if (formOptions.validation.validators) {
// validators: add validator with expressions
// TODO: use widget
const validatorsKey = Object.keys(formOptions.validation.validators);
validatorsKey.map(validatorKey => {
const validator = formOptions.validation.validators[validatorKey];
Expand Down
129 changes: 126 additions & 3 deletions projects/rero/ng-core/src/lib/record/editor/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { FormControl } from '@angular/forms';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FormlyFieldConfig } from '@ngx-formly/core';
import moment from 'moment';
import { TranslateService } from '@ngx-translate/core';
import sha256 from 'crypto-js/sha256';
import { BehaviorSubject, isObservable } from 'rxjs';
import { EditorService } from './services/editor.service';
import { isEmpty, removeEmptyValues } from './utils';
import { RecordService } from '../record.service';


export class NgCoreFormlyExtension {
Expand Down Expand Up @@ -48,7 +51,7 @@ export class NgCoreFormlyExtension {
* Constructor
* @param _editorService - editor service
*/
constructor(private _editorService: EditorService) { }
constructor(private _editorService: EditorService, private _recordService: RecordService) { }

/**
* prePopulate Formly hook
Expand All @@ -58,6 +61,7 @@ export class NgCoreFormlyExtension {
if (field.key) {
field.id = this._getKey(field);
}
this._setCustomValidators(field);
}

/**
Expand Down Expand Up @@ -175,6 +179,125 @@ export class NgCoreFormlyExtension {
});
}
}

private _setCustomValidators(field: FormlyFieldConfig) {
if (field.templateOptions == null || field.templateOptions.customValidators == null) {
return;
}
const customValidators = field.templateOptions.customValidators ? field.templateOptions.customValidators : {};
// asyncValidators: valueAlreadyExists
if (customValidators.valueAlreadyExists) {
const remoteRecordType =
customValidators.valueAlreadyExists.remoteRecordType;
const limitToValues =
customValidators.valueAlreadyExists.limitToValues;
const filter =
customValidators.valueAlreadyExists.filter;
const term = customValidators.valueAlreadyExists.term;
field.asyncValidators = {
validation: [
(control: FormControl) => {
return this._recordService.uniqueValue(
field,
remoteRecordType ? remoteRecordType : field.templateOptions.recordType,
field.templateOptions.pid ? field.templateOptions.pid : null,
term ? term : null,
limitToValues ? limitToValues : [],
filter ? filter : null
);
}
]
};
delete customValidators.valueAlreadyExists;
}
// asyncValidators: valueKeysInObject
// This validator is similar to uniqueValidator but only check on some specific fields of array items.
if (customValidators.uniqueValueKeysInObject) {
field.validators = {
uniqueValueKeysInObject: {
expression: (control: FormControl) => {
// if value isn't an array or array contains less than 2 elements, no need to check
if (!(control.value instanceof Array) || control.value.length < 2) {
return true;
}
const keysToKeep = customValidators.uniqueValueKeysInObject.keys;
const uniqueItems = Array.from(
new Set(control.value.map((v: any) => {
const keys = keysToKeep.reduce((acc, elt) => {
acc[elt] = v[elt];
return acc;
}, {});
return JSON.stringify(keys);
})),
);
return uniqueItems.length === control.value.length;
}
}
};
}

// The start date must be less than the end date.
if (customValidators.dateMustBeLessThan) {
const startDate: string = customValidators.dateMustBeLessThan.startDate;
const endDate: string = customValidators.dateMustBeLessThan.endDate;
const strict: boolean = customValidators.dateMustBeLessThan.strict || false;
const updateOn: 'change' | 'blur' | 'submit' =
customValidators.dateMustBeLessThan.strict || 'blur';
field.validators = {
dateMustBeLessThan: {
updateOn,
expression: (control: FormControl) => {
const startDateFc = control.parent.get(startDate);
const endDateFc = control.parent.get(endDate);
if (startDateFc.value !== null && endDateFc.value !== null) {
const dateStart = moment(startDateFc.value, 'YYYY-MM-DD');
const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD');
const isMustLessThan = strict
? dateStart >= dateEnd ? false : true
: dateStart > dateEnd ? false : true;
if (isMustLessThan) {
endDateFc.setErrors(null);
endDateFc.markAsDirty();
}
return isMustLessThan;
}
return false;
}
}
};
}

// The end date must be greater than the start date.
if (customValidators.dateMustBeGreaterThan) {
const startDate: string = customValidators.dateMustBeGreaterThan.startDate;
const endDate: string = customValidators.dateMustBeGreaterThan.endDate;
const strict: boolean = customValidators.dateMustBeGreaterThan.strict || false;
const updateOn: 'change' | 'blur' | 'submit' =
customValidators.dateMustBeGreaterThan.strict || 'blur';
field.validators = {
datesMustBeGreaterThan: {
updateOn,
expression: (control: FormControl) => {
const startDateFc = control.parent.get(startDate);
const endDateFc = control.parent.get(endDate);
if (startDateFc.value !== null && endDateFc.value !== null) {
const dateStart = moment(startDateFc.value, 'YYYY-MM-DD');
const dateEnd = moment(endDateFc.value, 'YYYY-MM-DD');
const isMustBeGreaterThan = strict
? dateStart <= dateEnd ? true : false
: dateStart < dateEnd ? true : false;
if (isMustBeGreaterThan) {
startDateFc.setErrors(null);
startDateFc.markAsDirty();
}
return isMustBeGreaterThan;
}
return false;
}
}
};
}
}
}

export class TranslateExtension {
Expand Down Expand Up @@ -277,7 +400,7 @@ export class TranslateExtension {
* @param translate ngx-translate service
* @returns FormlyConfig object configuration
*/
export function registerNgCoreFormlyExtension(translate: TranslateService, editorService: EditorService) {
export function registerNgCoreFormlyExtension(translate: TranslateService, editorService: EditorService, recordService: RecordService) {
return {
// translate the default validators messages
// widely inspired from ngx-formly example
Expand Down Expand Up @@ -364,7 +487,7 @@ export function registerNgCoreFormlyExtension(translate: TranslateService, edito
extension: new TranslateExtension(translate)
}, {
name: 'ng-core',
extension: new NgCoreFormlyExtension(editorService)
extension: new NgCoreFormlyExtension(editorService, recordService)
}],
};
}
Loading

0 comments on commit e6e1fc6

Please sign in to comment.