json-schema-form / angular-schema-form

Generate forms from a JSON schema, with AngularJS!
https://json-schema-form.github.io/angular-schema-form
MIT License
2.47k stars 654 forks source link

'date' as input field type produces 'Invalid type, expected date' #709

Open repolevedavaj opened 8 years ago

repolevedavaj commented 8 years ago

First: Thanks for this really cool and easy to use framework!

I would like to use a input field of type 'date' to support browser specific date picker implementations, but it always shows the error 'Invalid type, expected date' and i do not know where this comes from. If I insert an date input field manually and bind it with ng-model to a date object, it seems no problem.

I use the following schema

{
    type: "object",
    properties: {
        birthday: {
            type: "date"
        }
    }
}

form

[
    "*"
]

and configure the schemaFormProvider like this:

angular.module('schemaForm').config(['schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfPathProvider',
    function (schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) {
        var datepicker = function (name, schema, options) {
            if (schema.type === 'date') {
                var f = schemaFormProvider.stdFormObj(name, schema, options);
                f.key = options.path;
                f.type = 'date';
                options.lookup[sfPathProvider.stringify(options.path)] = f;
                return f;
            }
        };
        schemaFormProvider.defaults.date = [];
        schemaFormProvider.defaults.date.unshift(datepicker);
    }
]);

Could you tell me where I am going wrong?

kyse commented 8 years ago

Sounds like a validation error? Ie, your decorator is creating a date input field that is throwing a date as a string on the model, but validation is expecting an actual js Date object on the model? Make sure whatever date input being generated for that schema object is setup to add a date object on the model. Generally date inputs will be of type string with format of datetime or something along those lines. This will allow validation to not throw that error when the string of the date gets applied to the model.

repolevedavaj commented 8 years ago

The reason why i do not want to use input of type string is because it would not support the mobile browser specific implementation of date picker. But I think I found the reason why it is throwing a validation error. While validating, the function validateType(...) in the tv4.js is called, where the type of the validated data is determined. In my case the validated data is a js Date object, but is considered as of type object. So to solve my issue, it changed some small things:

{
    type: "object",
    properties: {
        birthday: {
            type: "object", // object so it matches the type determined in tv4.js
            format: "date"
        }
    }
}
angular.module('schemaForm').config(['schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfPathProvider',
    function (schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) {
        var datepicker = function (name, schema, options) {
            if (schema.type === 'object' && schema.format === "date") {
                var f = schemaFormProvider.stdFormObj(name, schema, options);
                f.key = options.path;
                f.type = 'date';
                options.lookup[sfPathProvider.stringify(options.path)] = f;
                return f;
            }
        };
        schemaFormProvider.defaults.object = [];
        schemaFormProvider.defaults.object.unshift(datepicker);
    }
]);

Now the date from the input field is successfully saved in the model. I do not know if it is the final solution that I will use, but is is working for now.

brianpkelley commented 8 years ago

How I fixed it was: Add this conditional to the /angular-schema-form/src/services/validator.js around line 39

    // Date values
    if ( schema.type === 'string' && schema.format === 'date' ) {
        if ( value === null ) {
            value = undefined;
        } else {
            if ( typeof value.toISOString === 'function' ) {
                value = value.toISOString();
            }
        }
    }
krptodr commented 7 years ago

@brianpkelley This fixes the issue, but introduces an issue with the validation. After you clear the field the validation sticks saying it's successful.

I was able to resolve the validation remaining green by adding this:

 if (schema.type === 'string' && schema.format === 'date' && viewValue === null  && !form.required) {
            ngModel.setPristine();
              return undefined;
          }

to line 2769 in the angular-schema-form/dist/schema-form.js of the release from the master branch.

Line 2769 starts with the following code for checking if the result is valid:

 if (!result.valid) {
            // it is invalid, return undefined (no model update)
            ngModel.$setValidity('tv4-' + result.error.code, false);
            error = result.error;

            // In Angular 1.3+ return the viewValue, otherwise we inadvertenly
            // will trigger a 'parse' error.
            // we will stop the model value from updating with our own $validator
            // later.
            if (ngModel.$validators) {
              return viewValue;
            }
            // Angular 1.2 on the other hand lacks $validators and don't add a 'parse' error.
            return undefined;
          }
          return viewValue;
        };
brianpkelley commented 7 years ago

https://github.com/json-schema-form/angular-schema-form-material/issues/18#issuecomment-239635625 I don't recall the exact circumstances that led me to the solution I posted on the link above, but it may offer more help if needed.

krptodr commented 7 years ago

@brianpkelley I'm having a hard time thinking it's best to solve this issue by adding a tv4 format and not adding a solution directly into ASF. It seems @Anthropic really favors your choice. But it seems like creating a decorator to resolve an issue isn't the right path, for something that is provided by default.

However, because I am using a decorator for my date fields ANYWAYS, I have chosen to implement your changes. While this does properly validate the date, with the exception of the validation message not applying the correct message as stated by @brianpkelley's solution in the other thread ema-form/angular-schema-form-material#18 (comment). It is expecting a valid of type object even if the field is not required and shows the field as invalid.

Here is my implementation:

// ISO Format - 2016-08-02T17:03:18.608Z - new Date().toISOString()
    var dateFormat = /^[0-9]{4,}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+|)(?:[+-][0-9]{2}:?(?:[0-9]{2}|)|Z)$/;
    // Standard Format - Tue Aug 02 2016 12:03:59 GMT-0500 (CDT) - new Date().toString()
    var mdDateFormat = /^(:?[A-Z][a-z]{2}\s){2}\d{1,2}\s\d{4}\s(:?\d{2}\:?){3}\s[A-Z]{3}\-\d{4}\s\([A-Z]{3}\)$/;

    var formats = {
        date: function (value) {
            if ( value && typeof value !== 'string' && value.toISOString ) {
                value = value.toISOString() || '';
            }

            if (dateFormat.test(value)  ) {//|| !mdDateFormat.test(value)
                return null;
            }

            return 'A valid date expected';
        }
    };
    tv4.addFormat( 'date', formats.date );    

 function dateObjectDefault(name, schema, options) {
      if (schema.type === 'object' && (schema.format === 'date' || schema.format === 'date-time')) {
        var f = schemaFormProvider.stdFormObj(name, schema, options);
        f.key  = options.path;
        f.type = 'date';
        options.lookup[sfPathProvider.stringify(options.path)] = f;
        return f;
      }
    };

    schemaFormProvider.defaults.object.unshift(dateObjectDefault);

Above, I used @brianpkelley's solution.

While adding my solution to line 2890 of Angular-schema-form.js Or rather line 41 of src/directives/schema-validate.directive.js

 if ((schema.type === 'object' || schema.type === 'string') && schema.format === 'date' && viewValue === null && !form.required) {
              //ngModel.$setValidity('tv4-', null);

              ngModel.$setPristine();
              return undefined;
          }

This only solves one of the many notable issues, you can see while playing around with validation on a date field. For instance, you can even have a date required, and not provide a value and still have it validate successfully.

Anthropic commented 7 years ago

@krptodr I would love to come up with a solution to this that works for everyone, dates are a pain, I assume that's why Textalk made the original datepicker add-on for the library as a separate item. I would like to add a stable datepicker to the bootstrap repo and whatever changes are required here, ideally if there was a main datepicker as part of bootstrap (I don't use bootstrap much so I have never checked) it would be the preferred choice and any behaviour here would have to cater for that. It just isn't high on my TODO list so it wouldn't be for a while unless someone wants to make a PR with one.

tivie commented 7 years ago

Since json schema draft 4 does not support custom types AND has a built in string date-time format (ISO8601), maybe the best way to tackle this is to force conversion of date objects to strings prior to validation.

So, if a jsonSchema is defined as this:

"mydate": {
  "id": "/properties/mydate",
  "type": "string",
  "format": "date-time"
}

angular-schema-form could coerce the date object into a string, somewhere prior to calling tv4 validation:

if (value instanceof Date) {
  value = value.toISOString();
}