From e60a44c47553de0f7d383dfdd72668f872417107 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 15 Dec 2013 22:26:55 -0500 Subject: [PATCH] feat(input) add support for datetime-local partially closes #757 --- src/ng/directive/input.js | 150 ++++++++++++++++++++++++++++++--- test/ng/directive/inputSpec.js | 117 +++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 10 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 36b5d882f8e5..ba2d83f34dac 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -12,6 +12,7 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\ 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 DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/; var inputType = { @@ -156,6 +157,71 @@ var inputType = { */ 'date': dateInputType, + /** + * @ngdoc inputType + * @name ng.directive:input.dateTimeLocal + * + * @description + * HTML5 or text input with datetime 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 + * local datetime format (yyyy-MM-ddTHH:mm), for example: `2010-12-28T14:57`. Will also accept a valid ISO + * datetime string 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('2010-12-28T14:57'); + 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-01T23:59'); + expect(binding('value')).toEqual(''); + expect(binding('myForm.input.$valid')).toEqual('false'); + }); + +
+ */ + 'datetime-local': dateTimeLocalInputType, /** * @ngdoc inputType * @name ng.directive:input.number @@ -603,7 +669,77 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } -function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) { +function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); + + ctrl.$parsers.push(function(value) { + if(ctrl.$isEmpty(value)) { + ctrl.$setValidity('datetimelocal', true); + return value; + } + + if(DATETIMELOCAL_REGEXP.test(value)) { + ctrl.$setValidity('datetimelocal', true); + return new Date(getTime(value)); + } + + ctrl.$setValidity('datetimelocal', false); + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if(isDate(value)) { + return $filter('date')(value, 'yyyy-MM-ddTHH:mm'); + } + 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)) { + DATETIMELOCAL_REGEXP.lastIndex = 0; + var parts = DATETIMELOCAL_REGEXP.exec(iso), + yyyy = +parts[1], + MM = +parts[2] - 1, + dd = +parts[3], + HH = +parts[4], + mm = +parts[5]; + + return +new Date(yyyy, MM, dd, HH, mm); + } + + return NaN; + } +} + +function dateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { @@ -623,13 +759,7 @@ function dateInputType(scope, element, attr, ctrl, $sniffer, $browser) { 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 $filter('date')(value, 'yyyy-MM-dd'); } return ctrl.$isEmpty(value) ? '' : '' + value; }); @@ -950,14 +1080,14 @@ function checkboxInputType(scope, element, attr, ctrl) { */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) { return { restrict: 'E', require: '?ngModel', link: function(scope, element, attr, ctrl) { if (ctrl) { (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + $browser, $filter); } } }; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 312d9d17f767..901ca7515794 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -717,6 +717,123 @@ describe('input', function() { // INPUT TYPES + + describe('datetime-local', function () { + it('should set the view if the model is valid ISO8601 local datetime', function() { + compileInput(''); + + scope.$apply(function(){ + scope.lunchtime = '2013-12-16T11:30'; + }); + + expect(inputElm.val()).toBe('2013-12-16T11:30'); + }); + + it('should set the view if the model if a valid Date object.', function(){ + compileInput(''); + + scope.$apply(function (){ + scope.tenSecondsToNextYear = new Date(2013, 11, 31, 23, 59); + }); + + expect(inputElm.val()).toBe('2013-12-31T23:59'); + }); + + it('should set the model undefined if the view is invalid', function (){ + compileInput(''); + + scope.$apply(function (){ + scope.breakMe = new Date(2009, 0, 6, 16, 25); + }); + + expect(inputElm.val()).toBe('2009-01-06T16:25'); + + try { + //set to text for browsers with datetime-local validation. + inputElm[0].setAttribute('type', 'text'); + } catch(e) { + //for IE8 + } + + changeInputValueTo('stuff'); + expect(inputElm.val()).toBe('stuff'); + expect(scope.breakMe).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + describe('min', function (){ + beforeEach(function (){ + compileInput(''); + scope.$digest(); + }); + + it('should invalidate', function (){ + changeInputValueTo('1999-12-31T01:02'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); + + it('should validate', function (){ + changeInputValueTo('2000-01-01T23:02'); + expect(inputElm).toBeValid(); + expect(+scope.value).toBe(+new Date(2000, 0, 1, 23, 2)); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); + }); + + describe('max', function (){ + beforeEach(function (){ + compileInput(''); + scope.$digest(); + }); + + it('should invalidate', function (){ + changeInputValueTo('2019-12-31T01:02'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.max).toBeTruthy(); + }); + + it('should validate', function() { + changeInputValueTo('2000-01-01T01:02'); + expect(inputElm).toBeValid(); + expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2)); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); + }); + + it('should validate even if max value changes on-the-fly', function(done) { + scope.max = '2013-01-01T01:02'; + compileInput(''); + scope.$digest(); + + changeInputValueTo('2014-01-01T12:34'); + expect(inputElm).toBeInvalid(); + + scope.max = '2001-01-01T01:02'; + scope.$digest(function () { + expect(inputElm).toBeValid(); + done(); + }); + }); + + it('should validate even if min value changes on-the-fly', function(done) { + scope.min = '2013-01-01T01:02'; + compileInput(''); + scope.$digest(); + + changeInputValueTo('2010-01-01T12:34'); + expect(inputElm).toBeInvalid(); + + scope.min = '2014-01-01T01:02'; + scope.$digest(function () { + expect(inputElm).toBeValid(); + done(); + }); + }); + }); + describe('date', function () { it('should set the view if the model is valid ISO8601 date', function() { compileInput('');