Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 06d0955

Browse files
committed
feat(ngModel): update model on each key stroke (revert ngModelInstant)
It turns out that listening only on "blur" event is not sufficient in many scenarios, especially when you use form validation you always had to use ngModelnstant e.g. if you want to disable a button based on valid/invalid form. The feedback we got from our apps as well as external apps is that the ngModelInstant should be the default. In the future we might provide alternative ways of suppressing updates on each key stroke, but it's not going to be the default behavior. Apps already using the ngModelInstant can safely remove it from their templates. Input fields without ngModelInstant directive will start propagating the input changes into the model on each key stroke.
1 parent a22e069 commit 06d0955

File tree

8 files changed

+76
-149
lines changed

8 files changed

+76
-149
lines changed

docs/content/guide/dev_guide.forms.ngdoc

+4-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ In addition it provides {@link api/angular.module.ng.$compileProvider.directive.
2020
<doc:source>
2121
<div ng-controller="Controller">
2222
<form novalidate class="simple-form">
23-
Name: <input type="text" ng-model="user.name" ng-model-instant /><br />
23+
Name: <input type="text" ng-model="user.name" /><br />
2424
E-mail: <input type="email" ng-model="user.email" /><br />
2525
Gender: <input type="radio" ng-model="user.gender" value="male" />male
2626
<input type="radio" ng-model="user.gender" value="female" />female<br />
@@ -50,11 +50,7 @@ In addition it provides {@link api/angular.module.ng.$compileProvider.directive.
5050
</doc:example>
5151

5252

53-
Note that:
54-
55-
* the {@link api/angular.module.ng.$compileProvider.directive.ng-model-instant ng-model-instant} causes the `user.name` to be updated immediately.
56-
57-
* `novalidate` is used to disable browser's native form validation.
53+
Note that `novalidate` is used to disable browser's native form validation.
5854

5955

6056

@@ -76,7 +72,7 @@ This ensures that the user is not distracted with an error until after interacti
7672
<div ng-controller="Controller">
7773
<form novalidate class="css-form">
7874
Name:
79-
<input type="text" ng-model="user.name" ng-model-instant required /><br />
75+
<input type="text" ng-model="user.name" required /><br />
8076
E-mail: <input type="email" ng-model="user.email" required /><br />
8177
Gender: <input type="radio" ng-model="user.gender" value="male" />male
8278
<input type="radio" ng-model="user.gender" value="female" />female<br />
@@ -147,7 +143,7 @@ This allows us to extend the above example with these features:
147143

148144
<input type="checkbox" ng-model="user.agree" name="userAgree" required />
149145
I agree: <input ng-show="user.agree" type="text" ng-model="user.agreeSign"
150-
ng-model-instant required /><br />
146+
required /><br />
151147
<div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>
152148

153149
<button ng-click="reset()" disabled="{{isUnchanged(user)}}">RESET</button>

src/AngularPublic.js

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ function publishExternalAPI(angular){
9898
ngModel: ngModelDirective,
9999
ngList: ngListDirective,
100100
ngChange: ngChangeDirective,
101-
ngModelInstant: ngModelInstantDirective,
102101
required: requiredDirective,
103102
ngRequired: requiredDirective,
104103
ngValue: ngValueDirective

src/ng/directive/input.js

+45-76
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,43 @@ function isEmpty(value) {
367367
}
368368

369369

370-
function textInputType(scope, element, attr, ctrl) {
371-
element.bind('blur', function() {
372-
scope.$apply(function() {
373-
ctrl.$setViewValue(trim(element.val()));
370+
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
371+
372+
var listener = function() {
373+
var value = trim(element.val());
374+
375+
if (ctrl.$viewValue !== value) {
376+
scope.$apply(function() {
377+
ctrl.$setViewValue(value);
378+
});
379+
}
380+
};
381+
382+
// if the browser does support "input" event, we are fine
383+
if ($sniffer.hasEvent('input')) {
384+
element.bind('input', listener);
385+
} else {
386+
var timeout;
387+
388+
element.bind('keydown', function(event) {
389+
var key = event.keyCode;
390+
391+
// ignore
392+
// command modifiers arrows
393+
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
394+
395+
if (!timeout) {
396+
timeout = $browser.defer(function() {
397+
listener();
398+
timeout = null;
399+
});
400+
}
374401
});
375-
});
402+
403+
// if user paste into input using mouse, we need "change" event to catch it
404+
element.bind('change', listener);
405+
}
406+
376407

377408
ctrl.$render = function() {
378409
element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
@@ -448,8 +479,8 @@ function textInputType(scope, element, attr, ctrl) {
448479
}
449480
};
450481

451-
function numberInputType(scope, element, attr, ctrl) {
452-
textInputType(scope, element, attr, ctrl);
482+
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
483+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
453484

454485
ctrl.$parsers.push(function(value) {
455486
var empty = isEmpty(value);
@@ -510,8 +541,8 @@ function numberInputType(scope, element, attr, ctrl) {
510541
});
511542
}
512543

513-
function urlInputType(scope, element, attr, ctrl) {
514-
textInputType(scope, element, attr, ctrl);
544+
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
545+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
515546

516547
var urlValidator = function(value) {
517548
if (isEmpty(value) || URL_REGEXP.test(value)) {
@@ -527,8 +558,8 @@ function urlInputType(scope, element, attr, ctrl) {
527558
ctrl.$parsers.push(urlValidator);
528559
}
529560

530-
function emailInputType(scope, element, attr, ctrl) {
531-
textInputType(scope, element, attr, ctrl);
561+
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
562+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
532563

533564
var emailValidator = function(value) {
534565
if (isEmpty(value) || EMAIL_REGEXP.test(value)) {
@@ -709,13 +740,14 @@ function checkboxInputType(scope, element, attr, ctrl) {
709740
</doc:scenario>
710741
</doc:example>
711742
*/
712-
var inputDirective = [function() {
743+
var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
713744
return {
714745
restrict: 'E',
715746
require: '?ngModel',
716747
link: function(scope, element, attr, ctrl) {
717748
if (ctrl) {
718-
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl);
749+
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
750+
$browser);
719751
}
720752
}
721753
};
@@ -1004,69 +1036,6 @@ var ngChangeDirective = valueFn({
10041036
});
10051037

10061038

1007-
/**
1008-
* @ngdoc directive
1009-
* @name angular.module.ng.$compileProvider.directive.ng-model-instant
1010-
*
1011-
* @element input
1012-
*
1013-
* @description
1014-
* By default, Angular udpates the model only on `blur` event - when the input looses focus.
1015-
* If you want to update after every key stroke, use `ng-model-instant`.
1016-
*
1017-
* @example
1018-
* <doc:example>
1019-
* <doc:source>
1020-
* First name: <input type="text" ng-model="firstName" /><br />
1021-
* Last name: <input type="text" ng-model="lastName" ng-model-instant /><br />
1022-
*
1023-
* First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}})
1024-
* is updated immediately, because of using `ng-model-instant`.
1025-
* </doc:source>
1026-
* <doc:scenario>
1027-
* it('should update first name on blur', function() {
1028-
* input('firstName').enter('santa', 'blur');
1029-
* expect(binding('firstName')).toEqual('santa');
1030-
* });
1031-
*
1032-
* it('should update last name immediately', function() {
1033-
* input('lastName').enter('santa', 'keydown');
1034-
* expect(binding('lastName')).toEqual('santa');
1035-
* });
1036-
* </doc:scenario>
1037-
* </doc:example>
1038-
*/
1039-
var ngModelInstantDirective = ['$browser', function($browser) {
1040-
return {
1041-
require: 'ngModel',
1042-
link: function(scope, element, attr, ctrl) {
1043-
var handler = function() {
1044-
scope.$apply(function() {
1045-
ctrl.$setViewValue(trim(element.val()));
1046-
});
1047-
};
1048-
1049-
var timeout;
1050-
element.bind('keydown', function(event) {
1051-
var key = event.keyCode;
1052-
1053-
// command modifiers arrows
1054-
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
1055-
1056-
if (!timeout) {
1057-
timeout = $browser.defer(function() {
1058-
handler();
1059-
timeout = null;
1060-
});
1061-
}
1062-
});
1063-
1064-
element.bind('change input', handler);
1065-
}
1066-
};
1067-
}];
1068-
1069-
10701039
var requiredDirective = [function() {
10711040
return {
10721041
require: '?ngModel',

src/ngScenario/Scenario.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ function browserTrigger(element, type, keys) {
327327
(function(fn){
328328
var parentTrigger = fn.trigger;
329329
fn.trigger = function(type) {
330-
if (/(click|change|keydown|blur)/.test(type)) {
330+
if (/(click|change|keydown|blur|input)/.test(type)) {
331331
var processDefaults = [];
332332
this.each(function(index, node) {
333333
processDefaults.push(browserTrigger(node, type));

src/ngScenario/dsl.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,13 @@ angular.scenario.dsl('binding', function() {
198198
*/
199199
angular.scenario.dsl('input', function() {
200200
var chain = {};
201+
var supportInputEvent = 'oninput' in document.createElement('div');
201202

202203
chain.enter = function(value, event) {
203204
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
204205
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
205206
input.val(value);
206-
input.trigger(event || 'blur');
207+
input.trigger(event || supportInputEvent && 'input' || 'change');
207208
done();
208209
});
209210
};

test/BinderSpec.js

-14
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,6 @@ describe('Binder', function() {
142142
expect(html.indexOf('action="foo();"')).toBeGreaterThan(0);
143143
});
144144

145-
it('RepeaterAdd', inject(function($rootScope, $compile) {
146-
element = $compile('<div><input type="text" ng-model="item.x" ng-repeat="item in items"></div>')($rootScope);
147-
$rootScope.items = [{x:'a'}, {x:'b'}];
148-
$rootScope.$apply();
149-
var first = childNode(element, 1);
150-
var second = childNode(element, 2);
151-
expect(first.val()).toEqual('a');
152-
expect(second.val()).toEqual('b');
153-
154-
first.val('ABC');
155-
browserTrigger(first, 'blur');
156-
expect($rootScope.items[0].x).toEqual('ABC');
157-
}));
158-
159145
it('ItShouldRemoveExtraChildrenWhenIteratingOverHash', inject(function($rootScope, $compile) {
160146
element = $compile('<div><div ng-repeat="i in items">{{i}}</div></div>')($rootScope);
161147
var items = {};

test/ng/directive/formSpec.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
describe('form', function() {
4-
var doc, control, scope, $compile;
4+
var doc, control, scope, $compile, changeInputValue;
55

66
beforeEach(module(function($compileProvider) {
77
$compileProvider.directive('storeModelCtrl', function() {
@@ -14,9 +14,14 @@ describe('form', function() {
1414
});
1515
}));
1616

17-
beforeEach(inject(function($injector) {
17+
beforeEach(inject(function($injector, $sniffer) {
1818
$compile = $injector.get('$compile');
1919
scope = $injector.get('$rootScope');
20+
21+
changeInputValue = function(elm, value) {
22+
elm.val(value);
23+
browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change');
24+
};
2025
}));
2126

2227
afterEach(function() {
@@ -126,10 +131,8 @@ describe('form', function() {
126131
var inputA = doc.find('input').eq(0),
127132
inputB = doc.find('input').eq(1);
128133

129-
inputA.val('val1');
130-
browserTrigger(inputA, 'blur');
131-
inputB.val('val2');
132-
browserTrigger(inputB, 'blur');
134+
changeInputValue(inputA, 'val1');
135+
changeInputValue(inputB, 'val2');
133136

134137
expect(scope.firstName).toBe('val1');
135138
expect(scope.lastName).toBe('val2');

0 commit comments

Comments
 (0)