ngx-schema-form icon indicating copy to clipboard operation
ngx-schema-form copied to clipboard

Validation Errors propagation

Open lzoubek opened this issue 7 years ago • 13 comments

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

lzoubek avatar Sep 21 '17 06:09 lzoubek

yep, looks like a bug

ebrehault avatar Sep 23 '17 16:09 ebrehault

@ebrehault Can you provide some hint? I can try to fix it and provide PR

lzoubek avatar Sep 26 '17 09:09 lzoubek

@fbessou any idea on this?

ebrehault avatar Sep 28 '17 07:09 ebrehault

Hi @lzoubek, if your widget override the ngAfterViewInit method, it must call the base implementation which sets up the error propagation.

fbessou avatar Oct 08 '17 20:10 fbessou

I got the same problem but I don't even override ngAfterViewInit.

felixbaral avatar Oct 10 '17 09:10 felixbaral

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
}

daniele-pecora avatar May 08 '18 13:05 daniele-pecora

Errors doesn't "propagate" because of calling custom validator on single property, while the whole model is validated against schema multiple times.

Invis1ble avatar May 23 '18 09:05 Invis1ble

I think we should look at ZSchema's customValidator option and maybe replace current implementation with it.

Invis1ble avatar May 23 '18 09:05 Invis1ble

@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...

daniele-pecora avatar May 24 '18 04:05 daniele-pecora

Why not delegate the custom validation processing to each property FormControl and have Angular handle it?

jmsegrev avatar Jun 17 '18 22:06 jmsegrev

Was this issue fixed?

idanmalka avatar Sep 07 '20 11:09 idanmalka

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?

aahventures avatar Dec 31 '20 20:12 aahventures

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

daniele-pecora avatar Jan 05 '21 14:01 daniele-pecora