Skip to content

More complicated validation scenarios seems to be not supported #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
corvis opened this issue Dec 23, 2014 · 19 comments
Closed

More complicated validation scenarios seems to be not supported #185

corvis opened this issue Dec 23, 2014 · 19 comments

Comments

@corvis
Copy link

corvis commented Dec 23, 2014

The Problem

I have create user form which along with other fields contains password and passwordRepeat fields.
Validation should be triggered on blur and the goal is to get "Passwords should match" message near the passwordRepeat field.

Investigation

The first idea was to add custom keyword in JsonSchema which will compare values. Something like this:

{
  type: "object",
  required: ['password', 'confirmPassword'],
  properties: {
    password: {
      type: "string"
    },
    confirmPassword: {
      type: "string",
      equalTo: "password"
    }
  }
}

equalTo should be registered in tv4 on initialization stage.
However later I realized that angular-schema-form splits original schema by field so each field knows only about own piece of schema which makes impossible to reffer other fields.

Solution

To be honest I didn't find really good solution here. I just forked angular-schema-form repository and added optional callback function in form definition which will be invoked after default validation in case when tv4 decided that value is valid.
Here is usage example from my project:

{
  key: 'confirmPassword',
  title: 'Confirm password',
  type: "password",
  validationMessage: validationMessagesBuilder.build('Confirm password', null, {
    'notMatch': "Passwords do not match"
  }),
  afterValidated: function (value, fieldModel, formModel, schema) {
    if (formModel.password != value) {
      return 'notMatch';
    }
  }
}

It should return error code if there is an error and undefined otherwise.

So...

Does anyone have any better ideas? Maybe someone solved similar issue before... Maybe I missed something...

Anyway if you think that this approach is useful and correct I can prepare pull request.

Chears,
Dmitry

@mike-marcacci
Copy link
Contributor

Hi @corvis - that's a really great question. It's generally against the goals of this project to change the json-schema spec (which is actually an ietf standard). However, the ability you want is part of the v5 $data proposal for constant.

You would do something like:

{
    "type": "object",
    "properties": {
        "password": {"type": "string"},
        "confirm": {
            "constant": {"$data": "1/password"}
        }
    },
    "required": ["password", "confirm"]
}

I'm just about 100% sure that TV4 doesn't support this yet (although others like jjv do), so json-schema-form can't support this right now. However, I just wanted to mention this as the "correct" way to do this.

I believe there are some other workarounds provided by the form declaration that could allow you to add this functionality, but I'll defer to those more familiar with them. Either way, if you're using a server-side validator that supports constant and $data, you can declare it like above without breaking the client side.

@corvis
Copy link
Author

corvis commented Dec 23, 2014

Hi @mike-marcacci, thanks for really quick reply.
It seems like the problem is not only in validation engine but in schema-form itself. Each form input is isolated from whole schema, it knows only about own definition which makes impossible to use references. E.g. for our example validator of confirmPassword field will get the following schema:

{
  type: 'object', 
  properties: {
      "confirmPassword": { type: "string", constant: {"$data": "1/password"}}
  }
}

no password field will be there as such reference {"$data": "1/password"} will be invalid. Same problem as I described in Investigation section.

Checkout this code snippet from Schema-Form sources (factory sfValidator) which do actual validation. It creates wrap inserts the only property and passes it to tv4:

    var wrap = {type: 'object', 'properties': {}};
    var propName = form.key[form.key.length - 1];
    wrap.properties[propName] = schema;

    if (form.required) {
      wrap.required = [propName];
    }
    var valueWrap = {};
    if (angular.isDefined(value)) {
      valueWrap[propName] = value;
    }
    return tv4.validateResult(valueWrap, wrap);

@davidlgj
Copy link
Contributor

davidlgj commented Jan 7, 2015

Hi @corvis,

as @mike-marcacci said generally we don't add stuff to the schema :-). Its not as pretty as a proper schema solution but I'd say this is easiest to solve password fields is an add-on/custom type that type renders two inputs and handles the validation the "standard angular way" by injecting the ngModelController and adding a validation function to that.

@davidlgj
Copy link
Contributor

davidlgj commented Jan 7, 2015

Also I've been pondering adding some kind of support for Angular 1.3s async validators, probably by adding a form attribute to tackle the "check if this username is already taken" scenario.

@brian428
Copy link

brian428 commented Jan 9, 2015

I think this is really needed. As far as I can tell, right now, async validators are completely ignored. They run if you set them up, but they don't affect form validity once they return. And I don't see any way to alter the validity of the field or the form within the async validator success or failure callback. Even explicit calls to the ngModelController $setValidity don't seem to have any effect.

@corvis
Copy link
Author

corvis commented Jan 9, 2015

@davidlgj I added that option to from definition, not to the schema. Basically in the save way as validation messages are set.

Also here is one more pretty common usecase which will be nice to keep in mind:

We have 2 date\time fields: "Publication date" and "Valid due date". And that should be constraint which validates that "Publication date" value is less than "Valid due date"

@brian428
Copy link

brian428 commented Jan 9, 2015

In the meantime, if anyone has any ideas about how to wedge async validation into the schema form (particularly from within custom directive controllers), I'd love to hear them. I just keep running into walls trying to make it work. :-/

@burdiuz
Copy link

burdiuz commented Jan 11, 2015

@davidlgj you do not add stuff to schema but you modify it. If you will have logic that will not split or modify schema and use it as it was passed , that would solve this problem and many others. Currently I have same problem but with different scenario.
My schema is for address form and looks like:

{
          "type":"object",
          "properties":{
            "streetLine1":{
              "title":"Street",
              "type":"string",
              "maxLength": 44,
              "minLength": 3
            },
            "state":{
              "title":"State",
              "type":"string",
              "enum": ["NY", "CT"]
            },
            "postalCode":{
              "title":"ZIP",
              "type":"string",
              "pattern":"^[0-9]{5}(-[0-9]{4})?$",
              "validationMessage":"Please Provide Valid ZIP Code"
            },
            "isoCountry":{
              "title":"Country",
              "type":"string",
              "enum": ["US", "CA"]
            },
            "contact": {
              "type": "object",
              "description": "Contact object",
              "anyOf": [
                {
                  "properties": {
                    "firstName":{
                      "title":"First Name",
                      "maxLength":44,
                      "minLength":1,
                      "type":"string"
                    },
                    "lastName":{
                      "title":"Last Name",
                      "maxLength":44,
                      "minLength":2,
                      "type":"string"
                    },
                    "companyName":{
                      "title":"Company",
                      "maxLength":44,
                      "minLength":3,
                      "type":"string"
                    }
                  },
                  "required": [
                    "companyName"
                  ]
                }, {
                  "properties": {
                    "firstName":{
                      "title":"First Name",
                      "maxLength":44,
                      "minLength":1,
                      "type":"string"
                    },
                    "lastName":{
                      "title":"Last Name",
                      "maxLength":44,
                      "minLength":2,
                      "type":"string"
                    },
                    "companyName":{
                      "title":"Company",
                      "maxLength":44,
                      "minLength":3,
                      "type":"string"
                    }
                  },
                  "required": [
                    "firstName",
                    "lastName"
                  ]
                }
              ],
              "properties": {
                "middleInitial": {
                  "title": "Middle Initial",
                  "maxLength": 1,
                  "minLength": 1,
                  "type": "string"
                }
              }
            }
          },
          "required": ["contact", "isoCountry", "state", "postalCode", "streetLine1"]
        }

And here object that passes tv4 validation for such schema:

      var data = {
        isoCountry: "US",
        state: "CT",
        streetLine1: "Street 1",
        postalCode:"12345",
        contact: {
          firstName: "ololo",
          lastName: "zzz",
          "middleInitial": "s"
        }
      };

I've tried to use schema-form in different ways but it seems it won't work with such schemas because schema-form modifies it splitting and disabling complex validation. So, as I see it now, schema-form has many of JSON Schema features disabled out of the box.

@burdiuz
Copy link

burdiuz commented Jan 11, 2015

@corvis, look for custom decorators, and for postProcess function of schema-form to pass additional data to your form field.

@davidlgj
Copy link
Contributor

@brian428

I fiddled with $asyncValidators and I think I got it working in a custom type, this is my test code:

angular.module('test',['schemaForm','ui.ace']).config(function(schemaFormDecoratorsProvider) {

  schemaFormDecoratorsProvider.addMapping(
    'bootstrapDecorator',
    'asynctest',
    'async.html'
  );

}).run(function($templateCache){

  // Get and modify default templates
  var tmpl = $templateCache.get('directives/decorators/bootstrap/default.html');

  $templateCache.put(
    'async.html',
    tmpl.replace('type="{{form.type}}"', 'type="text" async-test')
  );


}).directive('asyncTest', function($q) {
  return {
    restrict: 'A',
    scope: false,
    require: 'ngModel',
    link: function(scope, attr, element, ngModel) {

      ngModel.$asyncValidators['test'] = function(modelValue, viewValue) {
          var value = modelValue || viewValue;

          var deferred = $q.defer();
          setTimeout(function() {

            if (value === 'foo') {
              deferred.resolve();
            } else {
              deferred.reject();
            }
            scope.$apply();
          }, 200)
          return deferred.promise;
      }
    }
  }
})

Hope that helps! Ping me if you got any questions

@avishnyak
Copy link

I found another interesting way to do this without having to modify the actual templates. Using Angular decorators we can do this:

    app.config(function ($provide) {
        $provide.decorator('schemaValidateDirective', function ($delegate) {
            var directive = $delegate[0];

            var link = directive.link;

            directive.compile = function () {
                return function (scope, element, attrs, ngModel) {
                    link.apply(this, arguments);

                    // ngModel.$asyncValidators is accessible here as well as the sync version.
                };
            };

            return $delegate;
        });
    });

@davidlgj
Copy link
Contributor

@avishnyak awesome solution!

@corvis are you satisfied with the decorator solution? If so please close the issue.

@brian428
Copy link

For me, it turns out that getting my custom directives to use the async validators was only half the battle. It turns out that right now, there's really no way to know when all the async validators (across the form as a whole) are finished. I have an open ticket with the Angular team on this (angular/angular.js#10768).

@ulion
Copy link
Contributor

ulion commented Feb 27, 2015

@avishnyak can you explain more about your code, how will it affect the validation of the angular-schema-form? will it affect all decorators used by the angular-schema-form, even if it do not contain ng-model directive in the decorator template? can it validate the whole schema to the whole model, or validate the sub-property which is 'object' type schema? if so, how can it reflect the error message to the certain input fields or section?

@avishnyak
Copy link

@ulion sure! The method I showed above is additive to the decorators that come with angular-schema-form. That portion is handled by this line link.apply(this, arguments);. If you do not want regular validations run for the entire form, just comment that line out.

If you do not use ng-model in your custom templates, it will just come in as undefined to this function (though I haven't tested this scenario).

Can it validate the whole schema to the whole model, or validate the sub-property which is 'object' type schema?

Unfortunately no. The way this was designed internally, only leaf nodes (ones that are not array or object) actually have validators on them. However, I was able to get around this in a slightly different way.

WARNING: This is copy-pasted from my project and modified to remove project-specific items. I haven't compiled this so there may be typo bugs

app.config(function ($provide) {
    $provide.decorator('sfSchemaDirective', function ($delegate) {
        var directive = $delegate[0],
            link = directive.link;

        directive.compile = function () {
            return function (scope) {
                link.apply(this, arguments);

                var objectKeys = (function () {
                        if (Object.keys) {
                            return Object.keys;
                        }

                        return function (o) {
                            var keys = [];

                            for (var i in o) {
                                if (o.hasOwnProperty(i)) {
                                    keys.push(i);
                                }
                            }

                            return keys;
                        };
                    })();

                function escapePathComponent(str) {
                    if (str.indexOf('/') === -1 && str.indexOf('~') === -1) {
                        return str;
                    }

                    return str.replace(/~/g, '~0').replace(/\//g, '~1');
                }

                function compareObjects(mirror, obj, changedPaths, path) {
                    var newKeys = objectKeys(obj),
                        oldKeys = objectKeys(mirror),
                        deleted = false;

                    for (var t = oldKeys.length - 1; t >= 0; t--) {
                        var key = oldKeys[t],
                            oldVal = mirror[key];

                        if (obj.hasOwnProperty(key)) {
                            var newVal = obj[key];

                            if (typeof oldVal === 'object' && oldVal !== null && typeof newVal === 'object' && newVal !== null) {
                                compareObjects(oldVal, newVal, changedPaths, path + '/' + escapePathComponent(key));
                            } else {
                                if (oldVal !== newVal) {
                                    changedPaths.push({ path: path + '/' + escapePathComponent(key), value: angular.copy(newVal) });
                                }
                            }
                        } else {
                            deleted = true; // property has been deleted
                        }
                    }

                    if (!deleted && newKeys.length === oldKeys.length) {
                        return;
                    }

                    var key2;

                    for (var u = 0; u < newKeys.length; u++) {
                        key2 = newKeys[u];

                        if (!mirror.hasOwnProperty(key2)) {
                            changedPaths.push({ path: path + '/' + escapePathComponent(key2), value: angular.copy(obj[key2]) });
                        }
                    }
                }

                var waitFor = {};

                scope.$watch(function () {
                    if (scope.model && scope.schema && scope.schema.type && (waitFor.model !== scope.model || waitFor.schema !== scope.schema) && Object.keys(scope.schema.properties).length > 0) {

                        waitFor.model = scope.model;
                        waitFor.schema = scope.schema;

                        if (angular.isFunction(waitFor.listener)) {
                            waitFor.listener();
                            return;
                        }

                        waitFor.listener = scope.$watch('model', function (newValue, oldValue) {
                            var changes = [], errors;

                            compareObjects(oldValue, newValue, changes, '');

                            for (var i = 0; i < changes.length; i++) {
                                        // Here you have access to the full schema with scope.schema
                                        //  You have access to all changed values with changes[i].value and changes[i].path
                                        // To set an error state here... 
                                        // scope.formCtrl[changes[i].path.substring(1)].$setValidity('schemaError', false);
                                        // TODO: Apply error message https://github.com/Textalk/angular-schema-form/blob/master/docs/index.md#validation-messages
                            }
                        }, true);
                    }
                });
            };
        };

        return $delegate;
    });
});

@ulion
Copy link
Contributor

ulion commented Feb 27, 2015

Thank you for your code. but I just still figure out where is the
validation code?

in jsonform they did the whole validation, then in the error message, there
is some key path info, then according to the key path, they locate the
error related input field or field set, then show message there.

why just can not angular-schema-form do this?

2015-02-28 1:57 GMT+08:00 Anton Vishnyak [email protected]:

@ulion https://github.com/ulion sure! The method I showed above is
additive to the decorators that come with angular-schema-form. That portion
is handled by this line link.apply(this, arguments);. If you do not want
regular validations run for the entire form, just comment that line out.

If you do not use ng-model in your custom templates, it will just come in
as undefined to this function (though I haven't tested this scenario).

Can it validate the whole schema to the whole model, or validate the
sub-property which is 'object' type schema?

Unfortunately no. The way this was designed internally, only leaf nodes
(ones that are not array or object) actually have validators on them.
However, I was able to get around this in a slightly different way.

WARNING: This is copy-pasted from my project and modified to remove
project-specific items. I haven't compiled this so there may be typo bugs

app.config(function ($provide) {
$provide.decorator('sfSchemaDirective', function ($delegate) {
var directive = $delegate[0],
link = directive.link;

    directive.compile = function () {
        return function (scope) {
            link.apply(this, arguments);

            var objectKeys = (function () {
                    if (Object.keys) {
                        return Object.keys;
                    }

                    return function (o) {
                        var keys = [];

                        for (var i in o) {
                            if (o.hasOwnProperty(i)) {
                                keys.push(i);
                            }
                        }

                        return keys;
                    };
                })();

            function escapePathComponent(str) {
                if (str.indexOf('/') === -1 && str.indexOf('~') === -1) {
                    return str;
                }

                return str.replace(/~/g, '~0').replace(/\//g, '~1');
            }

            function compareObjects(mirror, obj, changedPaths, path) {
                var newKeys = objectKeys(obj),
                    oldKeys = objectKeys(mirror),
                    deleted = false;

                for (var t = oldKeys.length - 1; t >= 0; t--) {
                    var key = oldKeys[t],
                        oldVal = mirror[key];

                    if (obj.hasOwnProperty(key)) {
                        var newVal = obj[key];

                        if (typeof oldVal === 'object' && oldVal !== null && typeof newVal === 'object' && newVal !== null) {
                            compareObjects(oldVal, newVal, changedPaths, path + '/' + escapePathComponent(key));
                        } else {
                            if (oldVal !== newVal) {
                                changedPaths.push({ path: path + '/' + escapePathComponent(key), value: angular.copy(newVal) });
                            }
                        }
                    } else {
                        deleted = true; // property has been deleted
                    }
                }

                if (!deleted && newKeys.length === oldKeys.length) {
                    return;
                }

                var key2;

                for (var u = 0; u < newKeys.length; u++) {
                    key2 = newKeys[u];

                    if (!mirror.hasOwnProperty(key2)) {
                        changedPaths.push({ path: path + '/' + escapePathComponent(key2), value: angular.copy(obj[key2]) });
                    }
                }
            }

            var waitFor = {};

            scope.$watch(function () {
                if (scope.model && scope.schema && scope.schema.type && (waitFor.model !== scope.model || waitFor.schema !== scope.schema) && Object.keys(scope.schema.properties).length > 0) {

                    waitFor.model = scope.model;
                    waitFor.schema = scope.schema;

                    if (angular.isFunction(waitFor.listener)) {
                        waitFor.listener();
                        return;
                    }

                    waitFor.listener = scope.$watch('model', function (newValue, oldValue) {
                        var changes = [], errors;

                        compareObjects(oldValue, newValue, changes, '');

                        for (var i = 0; i < changes.length; i++) {
                                    // Here you have access to the full schema with scope.schema
                                    //  You have access to all changed values with changes[i].value and changes[i].path
                                    // To set an error state here...
                                    // scope.formCtrl[changes[i].path.substring(1)].$setValidity('schemaError', false);
                                    // TODO: Apply error message https://github.com/Textalk/angular-schema-form/blob/master/docs/index.md#validation-messages
                        }
                    }, true);
                }
            });
        };
    };

    return $delegate;
});

});


Reply to this email directly or view it on GitHub
#185 (comment)
.

Ulion

@avishnyak
Copy link

Sorry, the code I pasted was for you to add your own validation code. That code would go towards the bottom where the comments are. That would allow you to achieve a similar thing to jsonform. You have access to the whole schema and form so you can do whatever you want really.

It doesn't do it because it is architected differently. I am on your side in that it makes it difficult to have advanced validation scenarios and hopefully a future version will have a better mechanism.

@plong0
Copy link

plong0 commented Mar 11, 2015

I've built a simple demo of a user registration form which includes:

  • asynchronous validation for unique username
  • confirm password field with validation to see that it matches its password field

Code: https://gist.github.com/plong0/b158b596961d18b52770

@ulion
Copy link
Contributor

ulion commented Mar 11, 2015

Thank you @plong0

I just wonder, for each single validation, we have to write directive? if
this is "angularjs" way, then I have to say, it's wrong.

some easy validation way has to be extended and supported, so the
validation can be easily added at by a simple function.

so, can this be done in some easy way?

2015-03-12 0:20 GMT+08:00 plong0 [email protected]:

I've built a simple demo of a user registration form which includes:

  • asynchronous validation for unique username
  • confirm password field with validation to see that it matches its
    password field

Code: https://gist.github.com/plong0/b158b596961d18b52770


Reply to this email directly or view it on GitHub
#185 (comment)
.

Ulion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants