diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index aaabd1033398..2c4fa37942be 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -3,6 +3,8 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; +var DATE_REGEXP = /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/; +var TIME_REGEXP = /^([0-2]\d)(:)([0-5]\d)(:)?([0-5]\d)?(\.\d+)?$/; var inputType = { @@ -222,6 +224,144 @@ var inputType = { 'url': urlInputType, + /** + * @ngdoc inputType + * @name ng.directive:input.date + * + * @description + * Text input with date validation. Sets the `date` validation error key if the content is not a + * valid date. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Date: + + Required! + + Not valid date! + value = {{value}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + it('should initialize to model', function() { + expect(binding('value')).toEqual('2012-08-14'); + expect(binding('myForm.input.$valid')).toEqual('true'); + }); + + it('should be invalid if empty', function() { + input('value').enter(''); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + + it('should be invalid if over max', function() { + input('value').enter('2012-09-30'); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + +
+ */ + 'date': dateInputType, + + + /** + * @ngdoc inputType + * @name ng.directive:input.time + * + * @description + * Text input with time validation. Sets the `time` validation error key if the content is not a + * valid time. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. + * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the + * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for + * patterns defined as scope expressions. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Time: + + Required! + + Not valid time! + value = {{value}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + it('should initialize to model', function() { + expect(binding('value')).toEqual('20:00'); + expect(binding('myForm.input.$valid')).toEqual('true'); + }); + + it('should be invalid if empty', function() { + input('value').enter(''); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + + it('should be invalid if over max', function() { + input('value').enter('23:00'); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + +
+ */ + 'time': timeInputType, + + /** * @ngdoc inputType * @name ng.directive:input.email @@ -586,6 +726,110 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$parsers.push(urlValidator); } +function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + // as the input event doesn't trigger from the calendar dialog in chrome, + // we add a binding to the change event + element.bind('change', function() { + element.triggerHandler('input'); + }); + + var dateValidator = function(value) { + if (isEmpty(value) || DATE_REGEXP.test(value)) { + ctrl.$setValidity('date', true); + return value === '' ? null : value; + } else { + ctrl.$setValidity('date', false); + return undefined; + } + }; + + ctrl.$formatters.push(dateValidator); + ctrl.$parsers.push(dateValidator); + + if (attr.min) { + var min = Date.parse(attr.min); + var minValidator = function(value) { + if (!isEmpty(value) && Date.parse(value) < min) { + ctrl.$setValidity('min', false); + return undefined; + } else { + ctrl.$setValidity('min', true); + return value; + } + }; + + ctrl.$parsers.push(minValidator); + ctrl.$formatters.push(minValidator); + } + + if (attr.max) { + var max = Date.parse(attr.max); + var maxValidator = function(value) { + if (!isEmpty(value) && Date.parse(value) > max) { + ctrl.$setValidity('max', false); + return undefined; + } else { + ctrl.$setValidity('max', true); + return value; + } + }; + + ctrl.$parsers.push(maxValidator); + ctrl.$formatters.push(maxValidator); + } +} + +function timeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var timeValidator = function(value) { + if (isEmpty(value) || TIME_REGEXP.test(value)) { + ctrl.$setValidity('time', true); + return value === '' ? null : value; + } else { + ctrl.$setValidity('time', false); + return undefined; + } + }; + + ctrl.$formatters.push(timeValidator); + ctrl.$parsers.push(timeValidator); + + if (attr.min) { + var min = Date.parse('2000-01-01T'+attr.min); + var minValidator = function(value) { + if (!isEmpty(value) && Date.parse('2000-01-01T'+value) < min) { + ctrl.$setValidity('min', false); + return undefined; + } else { + ctrl.$setValidity('min', true); + return value; + } + }; + + ctrl.$parsers.push(minValidator); + ctrl.$formatters.push(minValidator); + } + + if (attr.max) { + var max = Date.parse('2000-01-01T'+attr.max); + var maxValidator = function(value) { + if (!isEmpty(value) && Date.parse('2000-01-01T'+value) > max) { + ctrl.$setValidity('max', false); + return undefined; + } else { + ctrl.$setValidity('max', true); + return value; + } + }; + + ctrl.$parsers.push(maxValidator); + ctrl.$formatters.push(maxValidator); + } +} + function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 4dcb79a38d12..352e1377d52c 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -740,6 +740,155 @@ describe('input', function() { }); + describe('date', function() { + + it('should reset the model if view is invalid', function() { + compileInput(''); + + scope.$apply(function() { + scope.from = '2012-08-14'; + }); + expect(inputElm.val()).toBe('2012-08-14'); + + try { + // to allow non-date values, we have to change type so that + // the browser which have number validation will not interfere with + // this test. IE8 won't allow it hence the catch. + inputElm[0].setAttribute('type', 'text'); + } catch (e) {} + + changeInputValueTo('2012X'); + expect(inputElm.val()).toBe('2012X'); + expect(scope.from).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + it('should parse empty string to null', function() { + compileInput(''); + + scope.$apply(function() { + scope.from = '2012-08-14'; + }); + + changeInputValueTo(''); + expect(scope.from).toBeNull(); + expect(inputElm).toBeValid(); + }); + + + describe('min', function() { + + it('should validate', function() { + compileInput(''); + scope.$digest(); + + changeInputValueTo('2011-12-31'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.min).toBeTruthy(); + + changeInputValueTo('2012-02-01'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe('2012-02-01'); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); + }); + + + describe('max', function() { + + it('should validate', function() { + compileInput(''); + scope.$digest(); + + changeInputValueTo('2012-06-14'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.max).toBeTruthy(); + + changeInputValueTo('2012-02-14'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe('2012-02-14'); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); + }); + }); + + describe('time', function() { + + it('should reset the model if view is invalid', function() { + compileInput(''); + + scope.$apply(function() { + scope.from = '11:00'; + }); + expect(inputElm.val()).toBe('11:00'); + + try { + // to allow non-time values, we have to change type so that + // the browser which have number validation will not interfere with + // this test. IE8 won't allow it hence the catch. + inputElm[0].setAttribute('type', 'text'); + } catch (e) {} + + changeInputValueTo('11X'); + expect(inputElm.val()).toBe('11X'); + expect(scope.from).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + + it('should parse empty string to null', function() { + compileInput(''); + + scope.$apply(function() { + scope.from = '11:10'; + }); + + changeInputValueTo(''); + expect(scope.from).toBeNull(); + expect(inputElm).toBeValid(); + }); + + + describe('min', function() { + + it('should validate', function() { + compileInput(''); + scope.$digest(); + + changeInputValueTo('01:59'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.min).toBeTruthy(); + + changeInputValueTo('11:20'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe('11:20'); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); + }); + + + describe('max', function() { + + it('should validate', function() { + compileInput(''); + scope.$digest(); + + changeInputValueTo('11:20'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.max).toBeTruthy(); + + changeInputValueTo('01:59'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe('01:59'); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); + }); + }); + describe('radio', function() { it('should update the model', function() {