From 0a77f06d34856a30df94b994383d61c84287d786 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 3 Dec 2013 23:27:01 -0500 Subject: [PATCH] feat(input): add handling for date input partially closes #757 --- src/ng/directive/input.js | 140 +++++++++++++++++++++++++++++++++ test/ng/directive/inputSpec.js | 116 +++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 4e77397878dd..a9db67547d4b 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -11,6 +11,7 @@ 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,6}$/; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; +var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; var inputType = { @@ -89,6 +90,71 @@ var inputType = { */ 'text': textInputType, + /** + * @ngdoc inputType + * @name ng.directive:input.date + * + * @description + * HTML5 or text input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. The text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Will also accept a valid ISO date or Date object + * as model input, but will always output a Date object to the model. + * + * @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=} min Sets the `min` validation error key if the value entered is less than `min`. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * @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 {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ Pick a date between in 2013: + + + Required! + + Not a 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('2013-10-22'); + 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('2015-01-01'); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + +
+ */ + 'date': dateInputType, /** * @ngdoc inputType @@ -530,6 +596,80 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } +function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + ctrl.$parsers.push(function(value) { + if(ctrl.$isEmpty(value)) { + ctrl.$setValidity('date', true); + return value; + } + + if(DATE_REGEXP.test(value)) { + ctrl.$setValidity('date', true); + return new Date(getTime(value)); + } + + ctrl.$setValidity('date', false); + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if(isDate(value)) { + var year = value.getFullYear(), + month = value.getMonth() + 1, + day = value.getDate(); + + month = (month < 10 ? '0' : '') + month; + day = (day < 10 ? '0' : '') + day; + return year + '-' + month + '-' + day; + } + return ctrl.$isEmpty(value) ? '' : '' + value; + }); + + if(attr.min) { + var minValidator = function(value) { + var valid = ctrl.$isEmpty(value) || + (getTime(value) >= getTime(attr.min)); + ctrl.$setValidity('min', valid); + return valid ? value : undefined; + }; + + ctrl.$parsers.push(minValidator); + ctrl.$formatters.push(minValidator); + } + + if(attr.max) { + var maxValidator = function(value) { + var valid = ctrl.$isEmpty(value) || + (getTime(value) <= getTime(attr.max)); + ctrl.$setValidity('max', valid); + return valid ? value : undefined; + }; + + ctrl.$parsers.push(maxValidator); + ctrl.$formatters.push(maxValidator); + } + + function getTime(iso) { + if(isDate(iso)) { + return +iso; + } + + if(isString(iso)) { + DATE_REGEXP.lastIndex = 0; + var parts = DATE_REGEXP.exec(iso), + yyyy = +parts[1], + mm = +parts[2] - 1, + dd = +parts[3], + time = new Date(yyyy, mm, dd); + return +time; + } + + return NaN; + } +} + function numberInputType(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 26abceae1cfe..9e8ccdbaa372 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -752,6 +752,122 @@ describe('input', function() { // INPUT TYPES + describe('date', function () { + it('should set the view if the model is valid ISO8601 date', function() { + compileInput(''); + + scope.$apply(function(){ + scope.birthday = '1977-10-22'; + }); + + expect(inputElm.val()).toBe('1977-10-22'); + }); + + it('should set the view if the model if a valid Date object.', function(){ + compileInput(''); + + scope.$apply(function (){ + scope.christmas = new Date(2013, 11, 25); + }); + + expect(inputElm.val()).toBe('2013-12-25'); + }); + + it('should set the model undefined if the view is invalid', function (){ + compileInput(''); + + scope.$apply(function (){ + scope.arrMatey = new Date(2014, 8, 14); + }); + + expect(inputElm.val()).toBe('2014-09-14'); + + try { + //set to text for browsers with date validation. + inputElm[0].setAttribute('type', 'text'); + } catch(e) { + //for IE8 + } + + changeInputValueTo('1-2-3'); + expect(inputElm.val()).toBe('1-2-3'); + expect(scope.arrMatey).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + describe('min', function (){ + beforeEach(function (){ + compileInput(''); + scope.$digest(); + }); + + it('should invalidate', function (){ + changeInputValueTo('1999-12-31'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); + + it('should validate', function (){ + changeInputValueTo('2000-01-01'); + expect(inputElm).toBeValid(); + expect(+scope.value).toBe(+new Date(2000, 0, 1)); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); + }); + + describe('max', function (){ + beforeEach(function (){ + compileInput(''); + scope.$digest(); + }); + + it('should invalidate', function (){ + changeInputValueTo('2019-12-31'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.max).toBeTruthy(); + }); + + it('should validate', function() { + changeInputValueTo('2000-01-01'); + expect(inputElm).toBeValid(); + expect(+scope.value).toBe(+new Date(2000, 0, 1)); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); + }); + + it('should validate even if max value changes on-the-fly', function(done) { + scope.max = '2013-01-01'; + compileInput(''); + scope.$digest(); + + changeInputValueTo('2014-01-01'); + expect(inputElm).toBeInvalid(); + + scope.max = '2001-01-01'; + scope.$digest(function () { + expect(inputElm).toBeValid(); + done(); + }); + }); + + it('should validate even if min value changes on-the-fly', function(done) { + scope.min = '2013-01-01'; + compileInput(''); + scope.$digest(); + + changeInputValueTo('2010-01-01'); + expect(inputElm).toBeInvalid(); + + scope.min = '2014-01-01'; + scope.$digest(function () { + expect(inputElm).toBeValid(); + done(); + }); + }); + }); + describe('number', function() { it('should reset the model if view is invalid', function() {