diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index a9db67547d4b..c1c32d2013d9 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):(\d\d)$/;
var inputType = {
@@ -156,6 +157,72 @@ 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:ss), for example: `2010-12-28T14:57:12`. 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
+
+
+
+
+
+
+ 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
@@ -596,7 +663,78 @@ 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:ss');
+ }
+ 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],
+ ss = +parts[6];
+
+ return +new Date(yyyy, MM, dd, HH, mm, ss);
+ }
+
+ return NaN;
+ }
+}
+
+function dateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
ctrl.$parsers.push(function(value) {
@@ -616,13 +754,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;
});
@@ -912,14 +1044,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 9e8ccdbaa372..383c5a33bb30 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -752,6 +752,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:15';
+ });
+
+ expect(inputElm.val()).toBe('2013-12-16T11:30:15');
+ });
+
+ 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, 50);
+ });
+
+ expect(inputElm.val()).toBe('2013-12-31T23:59:50');
+ });
+
+ 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, 10);
+ });
+
+ expect(inputElm.val()).toBe('2009-01-06T16:25:10');
+
+ 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:03');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.$error.min).toBeTruthy();
+ });
+
+ it('should validate', function (){
+ changeInputValueTo('2000-01-01T23:02:01');
+ expect(inputElm).toBeValid();
+ expect(+scope.value).toBe(+new Date(2000, 0, 1, 23, 2, 1));
+ expect(scope.form.alias.$error.min).toBeFalsy();
+ });
+ });
+
+ describe('max', function (){
+ beforeEach(function (){
+ compileInput('');
+ scope.$digest();
+ });
+
+ it('should invalidate', function (){
+ changeInputValueTo('2019-12-31T01:02:03');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.$error.max).toBeTruthy();
+ });
+
+ it('should validate', function() {
+ changeInputValueTo('2000-01-01T01:02:03');
+ expect(inputElm).toBeValid();
+ expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3));
+ 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:03';
+ compileInput('');
+ scope.$digest();
+
+ changeInputValueTo('2014-01-01T12:34:56');
+ expect(inputElm).toBeInvalid();
+
+ scope.max = '2001-01-01T01:02:03';
+ 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:03';
+ compileInput('');
+ scope.$digest();
+
+ changeInputValueTo('2010-01-01T12:34:56');
+ expect(inputElm).toBeInvalid();
+
+ scope.min = '2014-01-01T01:02:03';
+ scope.$digest(function () {
+ expect(inputElm).toBeValid();
+ done();
+ });
+ });
+ });
+
describe('date', function () {
it('should set the view if the model is valid ISO8601 date', function() {
compileInput('');