diff --git a/src/jqLite.js b/src/jqLite.js index 73b76f930739..61955850178d 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -506,6 +506,8 @@ forEach('input,select,option,textarea,button,form,details'.split(','), function( var ALIASED_ATTR = { 'ngMinlength' : 'minlength', 'ngMaxlength' : 'maxlength', + 'ngMin' : 'min', + 'ngMax' : 'max', 'ngPattern' : 'pattern' }; diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 977be6c7bfa6..0c8d21a4b336 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-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; +var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; 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 WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; @@ -1036,6 +1037,15 @@ function createDateParser(regexp, mapping) { } if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) == '"' && iso.charAt(iso.length-1) == '"') { + iso = iso.substring(1, iso.length-1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } regexp.lastIndex = 0; parts = regexp.exec(iso); @@ -1082,16 +1092,30 @@ function createDateInputType(type, regexp, parseDate, format) { return ''; }); - if (attr.min) { + if (isDefined(attr.min) || attr.ngMin) { + var minVal; ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(attr.min) || parseDate(value) >= parseDate(attr.min); + return ctrl.$isEmpty(value) || isUndefined(minVal) || parseDate(value) >= minVal; }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); } - if (attr.max) { + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(attr.max) || parseDate(value) <= parseDate(attr.max); + return ctrl.$isEmpty(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function parseObservedDateValue(val) { + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; } }; } @@ -1128,16 +1152,36 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { return value; }); - if (attr.min) { + if (attr.min || attr.ngMin) { + var minVal; ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(attr.min) || value >= parseFloat(attr.min); + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; + + attr.$observe('min', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + minVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - if (attr.max) { + if (attr.max || attr.ngMax) { + var maxVal; ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(attr.max) || value <= parseFloat(attr.max); + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; + + attr.$observe('max', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 3c5e6d437cb1..2ddda2e6ea32 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2063,9 +2063,12 @@ describe('input', function() { }); describe('min', function (){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + scope = $rootScope; + $rootScope.minVal = '2013-01'; + compileInput(''); + })); it('should invalidate', function (){ changeInputValueTo('2012-12'); @@ -2080,12 +2083,27 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2013, 6, 1)); expect(scope.form.alias.$error.min).toBeFalsy(); }); + + it('should revalidate when the min value changes', function (){ + changeInputValueTo('2013-07'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.min).toBeFalsy(); + + scope.minVal = '2014-01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); }); describe('max', function(){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + scope = $rootScope; + $rootScope.maxVal = '2013-01'; + compileInput(''); + })); it('should validate', function (){ changeInputValueTo('2012-03'); @@ -2100,6 +2118,18 @@ describe('input', function() { expect(scope.value).toBeUndefined(); expect(scope.form.alias.$error.max).toBeTruthy(); }); + + it('should revalidate when the max value changes', function (){ + changeInputValueTo('2012-07'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.max).toBeFalsy(); + + scope.maxVal = '2012-01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.max).toBeTruthy(); + }); }); }); @@ -2204,9 +2234,12 @@ describe('input', function() { }); describe('min', function (){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + scope = $rootScope; + $rootScope.minVal = '2013-W01'; + compileInput(''); + })); it('should invalidate', function (){ changeInputValueTo('2012-W12'); @@ -2221,12 +2254,26 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2013, 0, 17)); expect(scope.form.alias.$error.min).toBeFalsy(); }); + + it('should revalidate when the min value changes', function (){ + changeInputValueTo('2013-W03'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.min).toBeFalsy(); + + scope.minVal = '2014-W01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); }); describe('max', function(){ - beforeEach(function (){ - compileInput(''); - }); + beforeEach(inject(function ($rootScope){ + $rootScope.maxVal = '2013-W01'; + scope = $rootScope; + compileInput(''); + })); it('should validate', function (){ changeInputValueTo('2012-W01'); @@ -2241,6 +2288,18 @@ describe('input', function() { expect(scope.value).toBeUndefined(); expect(scope.form.alias.$error.max).toBeTruthy(); }); + + it('should revalidate when the max value changes', function (){ + changeInputValueTo('2012-W03'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.max).toBeFalsy(); + + scope.maxVal = '2012-W01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.max).toBeTruthy(); + }); }); }); @@ -2363,9 +2422,12 @@ describe('input', function() { }); describe('min', function (){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + $rootScope.minVal = '2000-01-01T12:30:00'; + scope = $rootScope; + compileInput(''); + })); it('should invalidate', function (){ changeInputValueTo('1999-12-31T01:02:00'); @@ -2380,12 +2442,27 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2000, 0, 1, 23, 2, 0)); expect(scope.form.alias.$error.min).toBeFalsy(); }); + + it('should revalidate when the min value changes', function (){ + changeInputValueTo('2000-02-01T01:02:00'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.min).toBeFalsy(); + + scope.minVal = '2010-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); }); describe('max', function (){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + $rootScope.maxVal = '2019-01-01T01:02:00'; + scope = $rootScope; + compileInput(''); + })); it('should invalidate', function (){ changeInputValueTo('2019-12-31T01:02:00'); @@ -2400,9 +2477,21 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 0)); expect(scope.form.alias.$error.max).toBeFalsy(); }); + + it('should revalidate when the max value changes', function (){ + changeInputValueTo('2000-02-01T01:02:00'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.max).toBeFalsy(); + + scope.maxVal = '2000-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.max).toBeTruthy(); + }); }); - it('should validate even if max value changes on-the-fly', function(done) { + it('should validate even if max value changes on-the-fly', function() { scope.max = '2013-01-01T01:02:00'; compileInput(''); @@ -2410,13 +2499,17 @@ describe('input', function() { expect(inputElm).toBeInvalid(); scope.max = '2001-01-01T01:02:00'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.max = '2024-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); - it('should validate even if min value changes on-the-fly', function(done) { + it('should validate even if min value changes on-the-fly', function() { scope.min = '2013-01-01T01:02:00'; compileInput(''); @@ -2424,10 +2517,50 @@ describe('input', function() { expect(inputElm).toBeInvalid(); scope.min = '2014-01-01T01:02:00'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.min = '2009-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-max value changes on-the-fly', function() { + scope.max = '2013-01-01T01:02:00'; + compileInput(''); + + changeInputValueTo('2014-01-01T12:34:00'); + expect(inputElm).toBeInvalid(); + + scope.max = '2001-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.max = '2024-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-min value changes on-the-fly', function() { + scope.min = '2013-01-01T01:02:00'; + compileInput(''); + + changeInputValueTo('2010-01-01T12:34:00'); + expect(inputElm).toBeInvalid(); + + scope.min = '2014-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.min = '2009-01-01T01:02:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); }); @@ -2550,9 +2683,12 @@ describe('input', function() { }); describe('min', function (){ - beforeEach(function (){ - compileInput(''); - }); + var scope; + beforeEach(inject(function ($rootScope){ + $rootScope.minVal = '09:30:00'; + scope = $rootScope; + compileInput(''); + })); it('should invalidate', function (){ changeInputValueTo('01:02:00'); @@ -2567,6 +2703,18 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(1970, 0, 1, 23, 2, 0)); expect(scope.form.alias.$error.min).toBeFalsy(); }); + + it('should revalidate when the min value changes', function (){ + changeInputValueTo('23:02:00'); + expect(inputElm).toBeValid(); + expect(scope.form.alias.$error.min).toBeFalsy(); + + scope.minVal = '23:55:00'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.min).toBeTruthy(); + }); }); describe('max', function (){ @@ -2589,32 +2737,56 @@ describe('input', function() { }); }); - it('should validate even if max value changes on-the-fly', function(done) { - scope.max = '21:02:00'; + it('should validate even if max value changes on-the-fly', function() { + scope.max = '4:02:00'; compileInput(''); - changeInputValueTo('22:34:00'); + changeInputValueTo('05:34:00'); expect(inputElm).toBeInvalid(); - scope.max = '12:34:00'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.max = '06:34:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); - it('should validate even if min value changes on-the-fly', function(done) { + it('should validate even if min value changes on-the-fly', function() { scope.min = '08:45:00'; compileInput(''); changeInputValueTo('06:15:00'); expect(inputElm).toBeInvalid(); - scope.min = '13:50:00'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.min = '05:50:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-max value changes on-the-fly', function() { + scope.max = '4:02:00'; + compileInput(''); + + changeInputValueTo('05:34:00'); + expect(inputElm).toBeInvalid(); + + scope.max = '06:34:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-min value changes on-the-fly', function() { + scope.min = '08:45:00'; + compileInput(''); + + changeInputValueTo('06:15:00'); + expect(inputElm).toBeInvalid(); + + scope.min = '05:50:00'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); }); @@ -2735,6 +2907,23 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2000, 0, 1)); expect(scope.form.alias.$error.min).toBeFalsy(); }); + + it('should parse ISO-based date strings as a valid min date value', inject(function($rootScope) { + var scope = $rootScope.$new(); + var element = $compile('
' + + '' + + '
')(scope); + + var inputElm = element.find('input'); + + scope.value = new Date(2010, 1, 1, 0, 0, 0); + scope.min = new Date(2014, 10, 10, 0, 0, 0); + scope.$digest(); + + expect(scope.myForm.myControl.$error.min).toBeTruthy(); + + dealoc(element); + })); }); describe('max', function (){ @@ -2755,9 +2944,26 @@ describe('input', function() { expect(+scope.value).toBe(+new Date(2000, 0, 1)); expect(scope.form.alias.$error.max).toBeFalsy(); }); + + it('should parse ISO-based date strings as a valid max date value', inject(function($rootScope) { + var scope = $rootScope.$new(); + var element = $compile('
' + + '' + + '
')(scope); + + var inputElm = element.find('input'); + + scope.value = new Date(2020, 1, 1, 0, 0, 0); + scope.max = new Date(2014, 10, 10, 0, 0, 0); + scope.$digest(); + + expect(scope.myForm.myControl.$error.max).toBeTruthy(); + + dealoc(element); + })); }); - it('should validate even if max value changes on-the-fly', function(done) { + it('should validate even if max value changes on-the-fly', function() { scope.max = '2013-01-01'; compileInput(''); @@ -2765,13 +2971,17 @@ describe('input', function() { expect(inputElm).toBeInvalid(); scope.max = '2001-01-01'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.max = '2021-01-01'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); - it('should validate even if min value changes on-the-fly', function(done) { + it('should validate even if min value changes on-the-fly', function() { scope.min = '2013-01-01'; compileInput(''); @@ -2779,10 +2989,50 @@ describe('input', function() { expect(inputElm).toBeInvalid(); scope.min = '2014-01-01'; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.min = '2009-01-01'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-max value changes on-the-fly', function() { + scope.max = '2013-01-01'; + compileInput(''); + + changeInputValueTo('2014-01-01'); + expect(inputElm).toBeInvalid(); + + scope.max = '2001-01-01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.max = '2021-01-01'; + scope.$digest(); + + expect(inputElm).toBeValid(); + }); + + it('should validate even if ng-min value changes on-the-fly', function() { + scope.min = '2013-01-01'; + compileInput(''); + + changeInputValueTo('2010-01-01'); + expect(inputElm).toBeInvalid(); + + scope.min = '2014-01-01'; + scope.$digest(); + + expect(inputElm).toBeInvalid(); + + scope.min = '2009-01-01'; + scope.$digest(); + + expect(inputElm).toBeValid(); }); }); @@ -2894,18 +3144,69 @@ describe('input', function() { expect(scope.form.alias.$error.min).toBeFalsy(); }); - it('should validate even if min value changes on-the-fly', function(done) { + it('should validate even if min value changes on-the-fly', function() { scope.min = 10; compileInput(''); - changeInputValueTo('5'); + changeInputValueTo('15'); + expect(inputElm).toBeValid(); + + scope.min = 20; + scope.$digest(); expect(inputElm).toBeInvalid(); - scope.min = 0; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); - }); + scope.min = null; + scope.$digest(); + expect(inputElm).toBeValid(); + + scope.min = '20'; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.min = 'abc'; + scope.$digest(); + expect(inputElm).toBeValid(); + }); + }); + + describe('ngMin', function() { + + it('should validate', function() { + compileInput(''); + + changeInputValueTo('1'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.min).toBeTruthy(); + + changeInputValueTo('100'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe(100); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); + + it('should validate even if the ngMin value changes on-the-fly', function() { + scope.min = 10; + compileInput(''); + + changeInputValueTo('15'); + expect(inputElm).toBeValid(); + + scope.min = 20; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.min = null; + scope.$digest(); + expect(inputElm).toBeValid(); + + scope.min = '20'; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.min = 'abc'; + scope.$digest(); + expect(inputElm).toBeValid(); }); }); @@ -2926,7 +3227,7 @@ describe('input', function() { expect(scope.form.alias.$error.max).toBeFalsy(); }); - it('should validate even if max value changes on-the-fly', function(done) { + it('should validate even if max value changes on-the-fly', function() { scope.max = 10; compileInput(''); @@ -2934,10 +3235,61 @@ describe('input', function() { expect(inputElm).toBeValid(); scope.max = 0; - scope.$digest(function () { - expect(inputElm).toBeInvalid(); - done(); - }); + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.max = null; + scope.$digest(); + expect(inputElm).toBeValid(); + + scope.max = '4'; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.max = 'abc'; + scope.$digest(); + expect(inputElm).toBeValid(); + }); + }); + + describe('ngMax', function() { + + it('should validate', function() { + compileInput(''); + + changeInputValueTo('20'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeUndefined(); + expect(scope.form.alias.$error.max).toBeTruthy(); + + changeInputValueTo('0'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe(0); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); + + it('should validate even if the ngMax value changes on-the-fly', function() { + scope.max = 10; + compileInput(''); + + changeInputValueTo('5'); + expect(inputElm).toBeValid(); + + scope.max = 0; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.max = null; + scope.$digest(); + expect(inputElm).toBeValid(); + + scope.max = '4'; + scope.$digest(); + expect(inputElm).toBeInvalid(); + + scope.max = 'abc'; + scope.$digest(); + expect(inputElm).toBeValid(); }); });