diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc index c643a613e194..0c3a7fc34957 100644 --- a/docs/content/guide/dev_guide.forms.ngdoc +++ b/docs/content/guide/dev_guide.forms.ngdoc @@ -20,7 +20,7 @@ In addition it provides {@link api/angular.module.ng.$compileProvider.directive.
- Name:
+ Name:
E-mail:
Gender: male female
@@ -50,11 +50,7 @@ In addition it provides {@link api/angular.module.ng.$compileProvider.directive. -Note that: - - * the {@link api/angular.module.ng.$compileProvider.directive.ng-model-instant ng-model-instant} causes the `user.name` to be updated immediately. - - * `novalidate` is used to disable browser's native form validation. +Note that `novalidate` is used to disable browser's native form validation. @@ -76,7 +72,7 @@ This ensures that the user is not distracted with an error until after interacti
Name: -
+
E-mail:
Gender: male female
@@ -147,7 +143,7 @@ This allows us to extend the above example with these features: I agree:
+ required />
Please agree and sign.
diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 8597de9cd98a..1326bf8ce9f4 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -98,7 +98,6 @@ function publishExternalAPI(angular){ ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, - ngModelInstant: ngModelInstantDirective, required: requiredDirective, ngRequired: requiredDirective, ngValue: ngValueDirective diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 3d53f7caa73a..aab12e3423bf 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -367,12 +367,43 @@ function isEmpty(value) { } -function textInputType(scope, element, attr, ctrl) { - element.bind('blur', function() { - scope.$apply(function() { - ctrl.$setViewValue(trim(element.val())); +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + + var listener = function() { + var value = trim(element.val()); + + if (ctrl.$viewValue !== value) { + scope.$apply(function() { + ctrl.$setViewValue(value); + }); + } + }; + + // if the browser does support "input" event, we are fine + if ($sniffer.hasEvent('input')) { + element.bind('input', listener); + } else { + var timeout; + + element.bind('keydown', function(event) { + var key = event.keyCode; + + // ignore + // command modifiers arrows + if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + + if (!timeout) { + timeout = $browser.defer(function() { + listener(); + timeout = null; + }); + } }); - }); + + // if user paste into input using mouse, we need "change" event to catch it + element.bind('change', listener); + } + ctrl.$render = function() { element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); @@ -448,8 +479,8 @@ function textInputType(scope, element, attr, ctrl) { } }; -function numberInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); ctrl.$parsers.push(function(value) { var empty = isEmpty(value); @@ -510,8 +541,8 @@ function numberInputType(scope, element, attr, ctrl) { }); } -function urlInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); +function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); var urlValidator = function(value) { if (isEmpty(value) || URL_REGEXP.test(value)) { @@ -527,8 +558,8 @@ function urlInputType(scope, element, attr, ctrl) { ctrl.$parsers.push(urlValidator); } -function emailInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); +function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { + textInputType(scope, element, attr, ctrl, $sniffer, $browser); var emailValidator = function(value) { if (isEmpty(value) || EMAIL_REGEXP.test(value)) { @@ -709,13 +740,14 @@ function checkboxInputType(scope, element, attr, ctrl) { */ -var inputDirective = [function() { +var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { return { restrict: 'E', require: '?ngModel', link: function(scope, element, attr, ctrl) { if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl); + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, + $browser); } } }; @@ -1004,69 +1036,6 @@ var ngChangeDirective = valueFn({ }); -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.ng-model-instant - * - * @element input - * - * @description - * By default, Angular udpates the model only on `blur` event - when the input looses focus. - * If you want to update after every key stroke, use `ng-model-instant`. - * - * @example - * - * - * First name:
- * Last name:
- * - * First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}}) - * is updated immediately, because of using `ng-model-instant`. - *
- * - * it('should update first name on blur', function() { - * input('firstName').enter('santa', 'blur'); - * expect(binding('firstName')).toEqual('santa'); - * }); - * - * it('should update last name immediately', function() { - * input('lastName').enter('santa', 'keydown'); - * expect(binding('lastName')).toEqual('santa'); - * }); - * - *
- */ -var ngModelInstantDirective = ['$browser', function($browser) { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var handler = function() { - scope.$apply(function() { - ctrl.$setViewValue(trim(element.val())); - }); - }; - - var timeout; - element.bind('keydown', function(event) { - var key = event.keyCode; - - // command modifiers arrows - if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - - if (!timeout) { - timeout = $browser.defer(function() { - handler(); - timeout = null; - }); - } - }); - - element.bind('change input', handler); - } - }; -}]; - - var requiredDirective = [function() { return { require: '?ngModel', diff --git a/src/ngScenario/Scenario.js b/src/ngScenario/Scenario.js index cd3c335fa6de..7335d5deacc4 100644 --- a/src/ngScenario/Scenario.js +++ b/src/ngScenario/Scenario.js @@ -327,7 +327,7 @@ function browserTrigger(element, type, keys) { (function(fn){ var parentTrigger = fn.trigger; fn.trigger = function(type) { - if (/(click|change|keydown|blur)/.test(type)) { + if (/(click|change|keydown|blur|input)/.test(type)) { var processDefaults = []; this.each(function(index, node) { processDefaults.push(browserTrigger(node, type)); diff --git a/src/ngScenario/dsl.js b/src/ngScenario/dsl.js index 8a1bccb108a2..7f399394562e 100644 --- a/src/ngScenario/dsl.js +++ b/src/ngScenario/dsl.js @@ -198,12 +198,13 @@ angular.scenario.dsl('binding', function() { */ angular.scenario.dsl('input', function() { var chain = {}; + var supportInputEvent = 'oninput' in document.createElement('div'); chain.enter = function(value, event) { return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) { var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input'); input.val(value); - input.trigger(event || 'blur'); + input.trigger(event || supportInputEvent && 'input' || 'change'); done(); }); }; diff --git a/test/BinderSpec.js b/test/BinderSpec.js index 306cbc437320..6d5dd91e0f2f 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -142,20 +142,6 @@ describe('Binder', function() { expect(html.indexOf('action="foo();"')).toBeGreaterThan(0); }); - it('RepeaterAdd', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.items = [{x:'a'}, {x:'b'}]; - $rootScope.$apply(); - var first = childNode(element, 1); - var second = childNode(element, 2); - expect(first.val()).toEqual('a'); - expect(second.val()).toEqual('b'); - - first.val('ABC'); - browserTrigger(first, 'blur'); - expect($rootScope.items[0].x).toEqual('ABC'); - })); - it('ItShouldRemoveExtraChildrenWhenIteratingOverHash', inject(function($rootScope, $compile) { element = $compile('
{{i}}
')($rootScope); var items = {}; diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index 5c34b5ad34dd..491a31dba10b 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -1,7 +1,7 @@ 'use strict'; describe('form', function() { - var doc, control, scope, $compile; + var doc, control, scope, $compile, changeInputValue; beforeEach(module(function($compileProvider) { $compileProvider.directive('storeModelCtrl', function() { @@ -14,9 +14,14 @@ describe('form', function() { }); })); - beforeEach(inject(function($injector) { + beforeEach(inject(function($injector, $sniffer) { $compile = $injector.get('$compile'); scope = $injector.get('$rootScope'); + + changeInputValue = function(elm, value) { + elm.val(value); + browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; })); afterEach(function() { @@ -126,10 +131,8 @@ describe('form', function() { var inputA = doc.find('input').eq(0), inputB = doc.find('input').eq(1); - inputA.val('val1'); - browserTrigger(inputA, 'blur'); - inputB.val('val2'); - browserTrigger(inputB, 'blur'); + changeInputValue(inputA, 'val1'); + changeInputValue(inputB, 'val2'); expect(scope.firstName).toBe('val1'); expect(scope.lastName).toBe('val2'); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 734cb34dcc97..395f37c38a83 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -236,7 +236,7 @@ describe('NgModelController', function() { describe('ng-model', function() { it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)', - inject(function($compile, $rootScope) { + inject(function($compile, $rootScope, $sniffer) { var element = $compile('')($rootScope); $rootScope.$digest(); @@ -254,14 +254,14 @@ describe('ng-model', function() { expect(element.hasClass('ng-invalid-email')).toBe(true); element.val('invalid-again'); - browserTrigger(element, 'blur'); + browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change'); expect(element).toBeInvalid(); expect(element).toBeDirty(); expect(element.hasClass('ng-valid-email')).toBe(false); expect(element.hasClass('ng-invalid-email')).toBe(true); element.val('vojta@google.com'); - browserTrigger(element, 'blur'); + browserTrigger(element, $sniffer.hasEvent('input') ? 'input' : 'change'); expect(element).toBeValid(); expect(element).toBeDirty(); expect(element.hasClass('ng-valid-email')).toBe(true); @@ -282,7 +282,7 @@ describe('ng-model', function() { describe('input', function() { - var formElm, inputElm, scope, $compile; + var formElm, inputElm, scope, $compile, changeInputValueTo; function compileInput(inputHtml) { formElm = jqLite('' + inputHtml + ''); @@ -290,14 +290,14 @@ describe('input', function() { $compile(formElm)(scope); } - function changeInputValueTo(value) { - inputElm.val(value); - browserTrigger(inputElm, 'blur'); - } - - beforeEach(inject(function($injector) { + beforeEach(inject(function($injector, $sniffer) { $compile = $injector.get('$compile'); scope = $injector.get('$rootScope'); + + changeInputValueTo = function(value) { + inputElm.val(value); + browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); + }; })); afterEach(function() { @@ -379,7 +379,7 @@ describe('input', function() { it('should ignore input without ng-model attr', function() { compileInput(''); - browserTrigger(inputElm, 'blur'); + changeInputValueTo(''); expect(inputElm.hasClass('ng-valid')).toBe(false); expect(inputElm.hasClass('ng-invalid')).toBe(false); expect(inputElm.hasClass('ng-pristine')).toBe(false); @@ -715,7 +715,7 @@ describe('input', function() { expect(inputElm[1].checked).toBe(true); expect(inputElm[2].checked).toBe(false); - browserTrigger(inputElm[2]); + browserTrigger(inputElm[2], 'click'); expect(scope.color).toBe('blue'); }); @@ -735,7 +735,7 @@ describe('input', function() { expect(inputElm[0].checked).toBe(true); expect(inputElm[1].checked).toBe(false); - browserTrigger(inputElm[1]); + browserTrigger(inputElm[1], 'click'); expect(scope.value).toBe('red'); scope.$apply(function() { @@ -753,7 +753,7 @@ describe('input', function() { it('should ignore checkbox without ng-model attr', function() { compileInput(''); - browserTrigger(inputElm, 'blur'); + changeInputValueTo(''); expect(inputElm.hasClass('ng-valid')).toBe(false); expect(inputElm.hasClass('ng-invalid')).toBe(false); expect(inputElm.hasClass('ng-pristine')).toBe(false); @@ -851,7 +851,7 @@ describe('input', function() { compileInput(''); inputElm = formElm.find('textarea'); - browserTrigger(inputElm, 'blur'); + changeInputValueTo(''); expect(inputElm.hasClass('ng-valid')).toBe(false); expect(inputElm.hasClass('ng-invalid')).toBe(false); expect(inputElm.hasClass('ng-pristine')).toBe(false); @@ -1053,33 +1053,6 @@ describe('input', function() { }); - describe('ng-model-instant', function() { - - it('should bind keydown, change, input events', inject(function($browser) { - compileInput(''); - - inputElm.val('value1'); - browserTrigger(inputElm, 'keydown'); - - // should be async (because of keydown) - expect(scope.value).toBeUndefined(); - - $browser.defer.flush(); - expect(scope.value).toBe('value1'); - - inputElm.val('value2'); - browserTrigger(inputElm, 'change'); - expect(scope.value).toBe('value2'); - - if (msie < 9) return; - - inputElm.val('value3'); - browserTrigger(inputElm, 'input'); - expect(scope.value).toBe('value3'); - })); - }); - - describe('ng-value', function() { it('should evaluate and set constant expressions', function() {