ngx-schema-form
ngx-schema-form copied to clipboard
Validation Errors propagation
Hello,
I have a schema, which works with list of objects. I have attached custom validators. I can see my validators being called and correctly validate (particular inputs get ng-invalid
class), but validation errors does not get propagated into rootProperty.
My schema
properties: {
Columns: {
type: 'array',
widget: {
id: 'arraytable',
},
items: {
type: 'object',
widget: 'schemaedit',
properties: {
Key: {
type: 'string',
title: 'Key',
description: 'Column Name/Key Must be unique within the schema'
}
}
}
}
}
My widgets 'arraytable' and 'schemaedit' basically just extend ArrayLayoutWidget
and ObjectLayoutWidget
.
I tried to debug schemaform and it looks like StringProperty
correctly emits _errorChanges
, but upper layer (ObjectProperty
) emits null
yep, looks like a bug
@ebrehault Can you provide some hint? I can try to fix it and provide PR
@fbessou any idea on this?
Hi @lzoubek, if your widget override the ngAfterViewInit method, it must call the base implementation which sets up the error propagation.
I got the same problem but I don't even override ngAfterViewInit.
My analysis and solution
The problem
This is what happens in angular2-schema-form
after a custom error is return by the custom validator
called from customValidator(this.value, this, this.findRoot())
public _runValidation(): any {
let errors = this.schemaValidator(this._value) || [];
let customValidator = this.validatorRegistry.get(this.path);
if (customValidator) {
let customErrors = customValidator(this.value, this, this.findRoot());
errors = this.mergeErrors(errors, customErrors);
}
if (errors.length === 0) {
errors = null;
}
this._errors = errors;
this.setErrors(this._errors);
}
private setErrors(errors) {
this._errors = errors;
this._errorsChanges.next(errors);
}
The custom error is recognized and stored just how expected (otherwise the custom message wouldn't appear), but the new validation state is not propagated to its parent form widgets control, so the corresponding parent widget is still valid.
First step ahead
I could make some progress in this by propagating the error by emitting changes in error by using property.errorsChanges.emit(error)
but, this doesn't show up the error message from custom validator.
Assume having registered a custom validator like this:
export const validations = {
"/customer/*/dateOfBirth": validateDateOfBirth
}
where the function is specified as
export const validateDateOfBirth = (value, property: FormProperty, form_current: ObjectProperty) => {
if (moment(value, 'YYYY-MM-DD').isAfter(moment(new Date()))) {
const error = createMessage(value, "The date must be in the past", property, form_current, true)
/*
* this propagates the validity state to its corresponding parent widget,
* but doesn't show up the message returned with this error
*/
property.errorsChanges.emit(error)
return error
}
return null
}
How does the regular validation work
What I further found out is that this seems not be a problem at all when using the regular validation.
e.g. pattern
, required
, maxLen
etc...
So I debugged into it and recognized that the validation is executed redundant. This means, no error from a child is ever propagated to the parent, but the parent re-validates the children since they are part of the parents schema definition. So every parent does this with its children.
parent: { // -- gets validated here and throws error
child_level1: { // -- gets validated here and throws error
child1_level2: { // -- gets validated here and throws error
child1_level3: { // -- gets validated here and throws error
...
}
}
}
}
Having knowledge about the children errors the parent does then get flagged as not valid.
To solve this issue we just have to do the same
My solution so far
So we just make sure that the parent get also their custom validator and return an error for their children.
Create a custom validator for every parent until you reach the final field and return an accurate error.
export const validations = {
"/customer": validateDateOfBirth_all,
"/customer/*": validateDateOfBirth_single,
"/customer/*/dateOfBirth": validateDateOfBirth
};
where the functions as specified as
export const validateDateOfBirth = (value, property, form_current) => {
if (moment(value, 'YYYY-MM-DD').isAfter(moment(new Date()))) {
const error = {
code: 'INVALID_DATE',
path: `#${property.path}`,
message: 'The date must be in the past',
params: [value]
}
return error
}
return null
}
export const validateDateOfBirth_single = (value, property, form_current) => {
// validate single customer property - dateOfBirth
return validateDateOfBirth(value['dateOfBirth'], {path: `${property.path}/dateOfBirth`}, form_current)
}
export const validateDateOfBirth_all = (value, property, form_current) => {
let errors
// iterate all customer
value.forEach((item, index, all) => {
const error = validateDateOfBirth_single(item, {path: `/customer/${index}`}, form_current)
if (error) {
errors = errors || []
errors.push(error)
}
})
return errors
}
Errors doesn't "propagate" because of calling custom validator on single property, while the whole model is validated against schema multiple times.
I think we should look at ZSchema's customValidator
option and maybe replace current implementation with it.
@Invis1ble
I think we should look at ZSchema's customValidator option and maybe replace current implementation with it.
I'm not sure about this, because angular2-schema-form allows you to use a complete different schema validator at all.
E.g using AJV schema validator instead would make all your ZSchema custom validations useless or you would have to re-write them.
I still really like the idea of having those custom validation processed by angular2-schema-form.
But still no other better solution for that problem...
Why not delegate the custom validation processing to each property FormControl and have Angular handle it?
Was this issue fixed?
So we just make sure that the parent get also their custom validator and return an error for their children.
Create a custom validator for every parent until you reach the final field and return an accurate error.
@daniele-pecora do you have a new work around or are you still using the same one?
So we just make sure that the parent get also their custom validator and return an error for their children. Create a custom validator for every parent until you reach the final field and return an accurate error.
@daniele-pecora do you have a new work around or are you still using the same one?
Still same state here