diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 4e77397878dd..6df1b05c7150 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -534,6 +534,10 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { + //on some browsers, '.' evaluates to NaN which prevents input of leading decimals + if (value === '.') { + value = '0.'; + } var empty = ctrl.$isEmpty(value); if (empty || NUMBER_REGEXP.test(value)) { ctrl.$setValidity('number', true); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 26abceae1cfe..e0dce7d036ae 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -409,1064 +409,1070 @@ describe('ngModel', function() { }); -describe('input', function() { - var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo; - - function compileInput(inputHtml) { - inputElm = jqLite(inputHtml); - formElm = jqLite('
'); - formElm.append(inputElm); - $compile(formElm)(scope); - } - - beforeEach(inject(function($injector, _$sniffer_, _$browser_) { - $sniffer = _$sniffer_; - $browser = _$browser_; - $compile = $injector.get('$compile'); - scope = $injector.get('$rootScope'); - - changeInputValueTo = function(value) { - inputElm.val(value); - browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); - }; - })); - - afterEach(function() { - dealoc(formElm); - }); +describe('input', function () { + var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo; + + function compileInput(inputHtml) { + inputElm = jqLite(inputHtml); + formElm = jqLite(''); + formElm.append(inputElm); + $compile(formElm)(scope); + } + beforeEach(inject(function ($injector, _$sniffer_, _$browser_) { + $sniffer = _$sniffer_; + $browser = _$browser_; + $compile = $injector.get('$compile'); + scope = $injector.get('$rootScope'); - it('should bind to a model', function() { - compileInput(''); + changeInputValueTo = function (value) { + inputElm.val(value); + browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; + })); - scope.$apply(function() { - scope.name = 'misko'; + afterEach(function () { + dealoc(formElm); }); - expect(inputElm.val()).toBe('misko'); - }); + it('should bind to a model', function () { + compileInput(''); - it('should not set readonly or disabled property on ie7', function() { - this.addMatchers({ - toBeOff: function(attributeName) { - var actualValue = this.actual.attr(attributeName); - this.message = function() { - return "Attribute '" + attributeName + "' expected to be off but was '" + actualValue + - "' in: " + angular.mock.dump(this.actual); - } + scope.$apply(function () { + scope.name = 'misko'; + }); - return !actualValue || actualValue == 'false'; - } + expect(inputElm.val()).toBe('misko'); }); - compileInput(''); - expect(inputElm.prop('readOnly')).toBe(false); - expect(inputElm.prop('disabled')).toBe(false); - expect(inputElm).toBeOff('readOnly'); - expect(inputElm).toBeOff('readonly'); - expect(inputElm).toBeOff('disabled'); - }); + it('should not set readonly or disabled property on ie7', function () { + this.addMatchers({ + toBeOff: function (attributeName) { + var actualValue = this.actual.attr(attributeName); + this.message = function () { + return "Attribute '" + attributeName + "' expected to be off but was '" + actualValue + + "' in: " + angular.mock.dump(this.actual); + } + return !actualValue || actualValue == 'false'; + } + }); - it('should update the model on "blur" event', function() { - compileInput(''); + compileInput(''); + expect(inputElm.prop('readOnly')).toBe(false); + expect(inputElm.prop('disabled')).toBe(false); - changeInputValueTo('adam'); - expect(scope.name).toEqual('adam'); - }); - - if (!(msie < 9)) { - describe('compositionevents', function() { - it('should not update the model between "compositionstart" and "compositionend" on non android', inject(function($sniffer) { - $sniffer.android = false; + expect(inputElm).toBeOff('readOnly'); + expect(inputElm).toBeOff('readonly'); + expect(inputElm).toBeOff('disabled'); + }); - compileInput(''); - changeInputValueTo('a'); - expect(scope.name).toEqual('a'); - browserTrigger(inputElm, 'compositionstart'); - changeInputValueTo('adam'); - expect(scope.name).toEqual('a'); - browserTrigger(inputElm, 'compositionend'); - changeInputValueTo('adam'); - expect(scope.name).toEqual('adam'); - })); - it('should update the model between "compositionstart" and "compositionend" on android', inject(function($sniffer) { - $sniffer.android = true; + it('should update the model on "blur" event', function () { + compileInput(''); - compileInput(''); - changeInputValueTo('a'); - expect(scope.name).toEqual('a'); - browserTrigger(inputElm, 'compositionstart'); changeInputValueTo('adam'); expect(scope.name).toEqual('adam'); - browserTrigger(inputElm, 'compositionend'); - changeInputValueTo('adam2'); - expect(scope.name).toEqual('adam2'); - })); }); - } - - describe('"change" event', function() { - function assertBrowserSupportsChangeEvent(inputEventSupported) { - // Force browser to report a lack of an 'input' event - $sniffer.hasEvent = function(eventName) { - if (eventName === 'input' && !inputEventSupported) { - return false; - } - return true; - }; - compileInput(''); - inputElm.val('mark'); - browserTrigger(inputElm, 'change'); - expect(scope.name).toEqual('mark'); + if (!(msie < 9)) { + describe('compositionevents', function () { + it('should not update the model between "compositionstart" and "compositionend" on non android', inject(function ($sniffer) { + $sniffer.android = false; + + compileInput(''); + changeInputValueTo('a'); + expect(scope.name).toEqual('a'); + browserTrigger(inputElm, 'compositionstart'); + changeInputValueTo('adam'); + expect(scope.name).toEqual('a'); + browserTrigger(inputElm, 'compositionend'); + changeInputValueTo('adam'); + expect(scope.name).toEqual('adam'); + })); + + it('should update the model between "compositionstart" and "compositionend" on android', inject(function ($sniffer) { + $sniffer.android = true; + + compileInput(''); + changeInputValueTo('a'); + expect(scope.name).toEqual('a'); + browserTrigger(inputElm, 'compositionstart'); + changeInputValueTo('adam'); + expect(scope.name).toEqual('adam'); + browserTrigger(inputElm, 'compositionend'); + changeInputValueTo('adam2'); + expect(scope.name).toEqual('adam2'); + })); + }); } - it('should update the model event if the browser does not support the "input" event',function() { - assertBrowserSupportsChangeEvent(false); - }); + describe('"change" event', function () { + function assertBrowserSupportsChangeEvent(inputEventSupported) { + // Force browser to report a lack of an 'input' event + $sniffer.hasEvent = function (eventName) { + if (eventName === 'input' && !inputEventSupported) { + return false; + } + return true; + }; + compileInput(''); + + inputElm.val('mark'); + browserTrigger(inputElm, 'change'); + expect(scope.name).toEqual('mark'); + } - it('should update the model event if the browser supports the "input" ' + - 'event so that form auto complete works',function() { - assertBrowserSupportsChangeEvent(true); - }); + it('should update the model event if the browser does not support the "input" event', function () { + assertBrowserSupportsChangeEvent(false); + }); - if (!_jqLiteMode) { - it('should not cause the double $digest when triggering an event using jQuery', function() { - $sniffer.hasEvent = function(eventName) { - return eventName !== 'input'; - }; + it('should update the model event if the browser supports the "input" ' + + 'event so that form auto complete works', function () { + assertBrowserSupportsChangeEvent(true); + }); - compileInput(''); + if (!_jqLiteMode) { + it('should not cause the double $digest when triggering an event using jQuery', function () { + $sniffer.hasEvent = function (eventName) { + return eventName !== 'input'; + }; + + compileInput(''); + + scope.field = 'fake field'; + scope.$watch('field', function () { + // We need to use _originalTrigger since trigger is modified by Angular Scenario. + inputElm._originalTrigger('change'); + }); + scope.$apply(); + }); + } + }); - scope.field = 'fake field'; - scope.$watch('field', function() { - // We need to use _originalTrigger since trigger is modified by Angular Scenario. - inputElm._originalTrigger('change'); + describe('"paste" and "cut" events', function () { + beforeEach(function () { + // Force browser to report a lack of an 'input' event + $sniffer.hasEvent = function (eventName) { + return eventName !== 'input'; + }; }); - scope.$apply(); - }); - } - }); - describe('"paste" and "cut" events', function() { - beforeEach(function() { - // Force browser to report a lack of an 'input' event - $sniffer.hasEvent = function(eventName) { - return eventName !== 'input'; - }; - }); + it('should update the model on "paste" event', function () { + compileInput(''); - it('should update the model on "paste" event', function() { - compileInput(''); + inputElm.val('mark'); + browserTrigger(inputElm, 'paste'); + $browser.defer.flush(); + expect(scope.name).toEqual('mark'); + }); - inputElm.val('mark'); - browserTrigger(inputElm, 'paste'); - $browser.defer.flush(); - expect(scope.name).toEqual('mark'); - }); + it('should update the model on "cut" event', function () { + compileInput(''); - it('should update the model on "cut" event', function() { - compileInput(''); + inputElm.val('john'); + browserTrigger(inputElm, 'cut'); + $browser.defer.flush(); + expect(scope.name).toEqual('john'); + }); - inputElm.val('john'); - browserTrigger(inputElm, 'cut'); - $browser.defer.flush(); - expect(scope.name).toEqual('john'); }); - }); - - it('should update the model and trim the value', function() { - compileInput(''); + it('should update the model and trim the value', function () { + compileInput(''); - changeInputValueTo(' a '); - expect(scope.name).toEqual('a'); - }); + changeInputValueTo(' a '); + expect(scope.name).toEqual('a'); + }); - it('should update the model and not trim the value', function() { - compileInput(''); + it('should update the model and not trim the value', function () { + compileInput(''); - changeInputValueTo(' a '); - expect(scope.name).toEqual(' a '); - }); + changeInputValueTo(' a '); + expect(scope.name).toEqual(' a '); + }); - it('should allow complex reference binding', function() { - compileInput(''); + it('should allow complex reference binding', function () { + compileInput(''); - scope.$apply(function() { - scope.obj = { abc: { name: 'Misko'} }; + scope.$apply(function () { + scope.obj = { abc: { name: 'Misko'} }; + }); + expect(inputElm.val()).toEqual('Misko'); }); - expect(inputElm.val()).toEqual('Misko'); - }); - it('should ignore input without ngModel directive', function() { - compileInput(''); + it('should ignore input without ngModel directive', function () { + compileInput(''); - changeInputValueTo(''); - expect(inputElm.hasClass('ng-valid')).toBe(false); - expect(inputElm.hasClass('ng-invalid')).toBe(false); - expect(inputElm.hasClass('ng-pristine')).toBe(false); - expect(inputElm.hasClass('ng-dirty')).toBe(false); - }); + changeInputValueTo(''); + expect(inputElm.hasClass('ng-valid')).toBe(false); + expect(inputElm.hasClass('ng-invalid')).toBe(false); + expect(inputElm.hasClass('ng-pristine')).toBe(false); + expect(inputElm.hasClass('ng-dirty')).toBe(false); + }); - it('should report error on assignment error', function() { - expect(function() { - compileInput(''); - scope.$digest(); - }).toThrowMinErr("$parse", "syntax", "Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); - }); + it('should report error on assignment error', function () { + expect(function () { + compileInput(''); + scope.$digest(); + }).toThrowMinErr("$parse", "syntax", "Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at ['']."); + }); + + it("should render as blank if null", function () { + compileInput(''); - it("should render as blank if null", function() { - compileInput(''); + scope.$apply(function () { + scope.age = null; + }); - scope.$apply(function() { - scope.age = null; + expect(scope.age).toBeNull(); + expect(inputElm.val()).toEqual(''); }); - expect(scope.age).toBeNull(); - expect(inputElm.val()).toEqual(''); - }); + it('should render 0 even if it is a number', function () { + compileInput(''); + scope.$apply(function () { + scope.value = 0; + }); - it('should render 0 even if it is a number', function() { - compileInput(''); - scope.$apply(function() { - scope.value = 0; + expect(inputElm.val()).toBe('0'); }); - expect(inputElm.val()).toBe('0'); - }); - - describe('pattern', function() { + describe('pattern', function () { - it('should validate in-lined pattern', function() { - compileInput(''); - scope.$digest(); + it('should validate in-lined pattern', function () { + compileInput(''); + scope.$digest(); - changeInputValueTo('x000-00-0000x'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('x000-00-0000x'); + expect(inputElm).toBeInvalid(); - changeInputValueTo('000-00-0000'); - expect(inputElm).toBeValid(); + changeInputValueTo('000-00-0000'); + expect(inputElm).toBeValid(); - changeInputValueTo('000-00-0000x'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('000-00-0000x'); + expect(inputElm).toBeInvalid(); - changeInputValueTo('123-45-6789'); - expect(inputElm).toBeValid(); + changeInputValueTo('123-45-6789'); + expect(inputElm).toBeValid(); - changeInputValueTo('x'); - expect(inputElm).toBeInvalid(); - }); + changeInputValueTo('x'); + expect(inputElm).toBeInvalid(); + }); - it('should validate in-lined pattern with modifiers', function() { - compileInput(''); - scope.$digest(); + it('should validate in-lined pattern with modifiers', function () { + compileInput(''); + scope.$digest(); - changeInputValueTo('aB'); - expect(inputElm).toBeValid(); + changeInputValueTo('aB'); + expect(inputElm).toBeValid(); - changeInputValueTo('xx'); - expect(inputElm).toBeInvalid(); - }); + changeInputValueTo('xx'); + expect(inputElm).toBeInvalid(); + }); - it('should validate pattern from scope', function() { - compileInput(''); - scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/; - scope.$digest(); + it('should validate pattern from scope', function () { + compileInput(''); + scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/; + scope.$digest(); - changeInputValueTo('x000-00-0000x'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('x000-00-0000x'); + expect(inputElm).toBeInvalid(); - changeInputValueTo('000-00-0000'); - expect(inputElm).toBeValid(); + changeInputValueTo('000-00-0000'); + expect(inputElm).toBeValid(); - changeInputValueTo('000-00-0000x'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('000-00-0000x'); + expect(inputElm).toBeInvalid(); - changeInputValueTo('123-45-6789'); - expect(inputElm).toBeValid(); + changeInputValueTo('123-45-6789'); + expect(inputElm).toBeValid(); - changeInputValueTo('x'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('x'); + expect(inputElm).toBeInvalid(); - scope.regexp = /abc?/; + scope.regexp = /abc?/; - changeInputValueTo('ab'); - expect(inputElm).toBeValid(); + changeInputValueTo('ab'); + expect(inputElm).toBeValid(); - changeInputValueTo('xx'); - expect(inputElm).toBeInvalid(); - }); + changeInputValueTo('xx'); + expect(inputElm).toBeInvalid(); + }); - it('should throw an error when scope pattern can\'t be found', function() { - expect(function() { - compileInput(''); - scope.$apply(); - }).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/); + it('should throw an error when scope pattern can\'t be found', function () { + expect(function () { + compileInput(''); + scope.$apply(); + }).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/); + }); }); - }); - describe('minlength', function() { + describe('minlength', function () { - it('should invalid shorter than given minlength', function() { - compileInput(''); + it('should invalid shorter than given minlength', function () { + compileInput(''); - changeInputValueTo('aa'); - expect(scope.value).toBeUndefined(); + changeInputValueTo('aa'); + expect(scope.value).toBeUndefined(); - changeInputValueTo('aaa'); - expect(scope.value).toBe('aaa'); + changeInputValueTo('aaa'); + expect(scope.value).toBe('aaa'); + }); }); - }); - describe('maxlength', function() { + describe('maxlength', function () { - it('should invalid shorter than given maxlength', function() { - compileInput(''); + it('should invalid shorter than given maxlength', function () { + compileInput(''); - changeInputValueTo('aaaaaaaa'); - expect(scope.value).toBeUndefined(); + changeInputValueTo('aaaaaaaa'); + expect(scope.value).toBeUndefined(); - changeInputValueTo('aaa'); - expect(scope.value).toBe('aaa'); + changeInputValueTo('aaa'); + expect(scope.value).toBe('aaa'); + }); }); - }); - // INPUT TYPES + // INPUT TYPES - describe('number', function() { + describe('number', function () { - it('should reset the model if view is invalid', function() { - compileInput(''); + it('should reset the model if view is invalid', function () { + compileInput(''); - scope.$apply(function() { - scope.age = 123; - }); - expect(inputElm.val()).toBe('123'); - - try { - // to allow non-number 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('123X'); - expect(inputElm.val()).toBe('123X'); - expect(scope.age).toBeUndefined(); - expect(inputElm).toBeInvalid(); - }); + scope.$apply(function () { + scope.age = 123; + }); + expect(inputElm.val()).toBe('123'); + try { + // to allow non-number 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) { } - it('should render as blank if null', function() { - compileInput(''); + changeInputValueTo('123X'); + expect(inputElm.val()).toBe('123X'); + expect(scope.age).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); - scope.$apply(function() { - scope.age = null; - }); - expect(scope.age).toBeNull(); - expect(inputElm.val()).toEqual(''); - }); + it('should render as blank if null', function () { + compileInput(''); + scope.$apply(function () { + scope.age = null; + }); - it('should come up blank when no value specified', function() { - compileInput(''); + expect(scope.age).toBeNull(); + expect(inputElm.val()).toEqual(''); + }); - scope.$digest(); - expect(inputElm.val()).toBe(''); - scope.$apply(function() { - scope.age = null; - }); + it('should come up blank when no value specified', function () { + compileInput(''); - expect(scope.age).toBeNull(); - expect(inputElm.val()).toBe(''); - }); + scope.$digest(); + expect(inputElm.val()).toBe(''); + scope.$apply(function () { + scope.age = null; + }); - it('should parse empty string to null', function() { - compileInput(''); + expect(scope.age).toBeNull(); + expect(inputElm.val()).toBe(''); + }); - scope.$apply(function() { - scope.age = 10; - }); - changeInputValueTo(''); - expect(scope.age).toBeNull(); - expect(inputElm).toBeValid(); - }); + it('should parse empty string to null', function () { + compileInput(''); + + scope.$apply(function () { + scope.age = 10; + }); + + changeInputValueTo(''); + expect(scope.age).toBeNull(); + expect(inputElm).toBeValid(); + }); + + it('should allow "." in input element', function () { + compileInput(''); + changeInputValueTo('.'); + expect(inputElm.val()).toBe('.'); + }); - describe('min', function() { + describe('min', function () { - it('should validate', function() { - compileInput(''); - scope.$digest(); + it('should validate', function () { + compileInput(''); + scope.$digest(); - changeInputValueTo('1'); - expect(inputElm).toBeInvalid(); - expect(scope.value).toBeFalsy(); - expect(scope.form.alias.$error.min).toBeTruthy(); + 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(); - }); + changeInputValueTo('100'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe(100); + expect(scope.form.alias.$error.min).toBeFalsy(); + }); - it('should validate even if min value changes on-the-fly', function(done) { - scope.min = 10; - compileInput(''); - scope.$digest(); + it('should validate even if min value changes on-the-fly', function (done) { + scope.min = 10; + compileInput(''); + scope.$digest(); - changeInputValueTo('5'); - expect(inputElm).toBeInvalid(); + changeInputValueTo('5'); + expect(inputElm).toBeInvalid(); - scope.min = 0; - scope.$digest(function () { - expect(inputElm).toBeValid(); - done(); + scope.min = 0; + scope.$digest(function () { + expect(inputElm).toBeValid(); + done(); + }); + }); }); - }); - }); - describe('max', function() { + describe('max', function () { - it('should validate', function() { - compileInput(''); - scope.$digest(); + it('should validate', function () { + compileInput(''); + scope.$digest(); - changeInputValueTo('20'); - expect(inputElm).toBeInvalid(); - expect(scope.value).toBeFalsy(); - expect(scope.form.alias.$error.max).toBeTruthy(); + changeInputValueTo('20'); + expect(inputElm).toBeInvalid(); + expect(scope.value).toBeFalsy(); + expect(scope.form.alias.$error.max).toBeTruthy(); - changeInputValueTo('0'); - expect(inputElm).toBeValid(); - expect(scope.value).toBe(0); - expect(scope.form.alias.$error.max).toBeFalsy(); - }); + changeInputValueTo('0'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe(0); + expect(scope.form.alias.$error.max).toBeFalsy(); + }); - it('should validate even if max value changes on-the-fly', function(done) { - scope.max = 10; - compileInput(''); - scope.$digest(); + it('should validate even if max value changes on-the-fly', function (done) { + scope.max = 10; + compileInput(''); + scope.$digest(); - changeInputValueTo('5'); - expect(inputElm).toBeValid(); + changeInputValueTo('5'); + expect(inputElm).toBeValid(); - scope.max = 0; - scope.$digest(function () { - expect(inputElm).toBeInvalid(); - done(); + scope.max = 0; + scope.$digest(function () { + expect(inputElm).toBeInvalid(); + done(); + }); + }); }); - }); - }); - describe('required', function() { + describe('required', function () { - it('should be valid even if value is 0', function() { - compileInput(''); + it('should be valid even if value is 0', function () { + compileInput(''); - changeInputValueTo('0'); - expect(inputElm).toBeValid(); - expect(scope.value).toBe(0); - expect(scope.form.alias.$error.required).toBeFalsy(); - }); + changeInputValueTo('0'); + expect(inputElm).toBeValid(); + expect(scope.value).toBe(0); + expect(scope.form.alias.$error.required).toBeFalsy(); + }); - it('should be valid even if value 0 is set from model', function() { - compileInput(''); + it('should be valid even if value 0 is set from model', function () { + compileInput(''); - scope.$apply(function() { - scope.value = 0; - }); + scope.$apply(function () { + scope.value = 0; + }); - expect(inputElm).toBeValid(); - expect(inputElm.val()).toBe('0') - expect(scope.form.alias.$error.required).toBeFalsy(); - }); + expect(inputElm).toBeValid(); + expect(inputElm.val()).toBe('0') + expect(scope.form.alias.$error.required).toBeFalsy(); + }); - it('should register required on non boolean elements', function() { - compileInput('