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
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+ 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() {