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

Commit 656c8fa

Browse files
authored
fix(input): listen on "change" instead of "click" for radio/checkbox ngModels
input[radio] and inout[checkbox] now listen on the change event instead of the click event. This fixes issue with 3rd party libraries that trigger a change event on inputs, e.g. Bootstrap 3 custom checkbox / radio button toggles. It also makes it easier to prevent specific events that can cause a checkbox / radio to change, e.g. click events. Previously, this was difficult because the custom click handler had to be registered before the input directive's click handler. It is possible that radio and checkbox listened to click because IE8 has broken support for listening on change, see http://www.quirksmode.org/dom/events/change.html Closes #4516 Closes #14667 Closes #14685 BREAKING CHANGE: `input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event. Most apps should not be affected, as "change" is automatically fired by browsers after "click" happens. Two scenarios might need migration: - Custom click events: Before this change, custom click event listeners on radio / checkbox would be called after the input element and `ngModel` had been updated, unless they were specifically registered before the built-in click handlers. After this change, they are called before the input is updated, and can call event.preventDefault() to prevent the input from updating. If an app uses a click event listener that expects ngModel to be updated when it is called, it now needs to register a change event listener instead. - Triggering click events: Conventional trigger functions: The change event might not be fired when the input element is not attached to the document. This can happen in **tests** that compile input elements and trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method, the change event will not be fired when the input isn't attached to the document. Before: ```js it('should update the model', inject(function($compile, $rootScope) { var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope); inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger() expect($rootScope.checkbox).toBe(true); }); ``` With this patch, `$rootScope.checkbox` might not be true, because the click event hasn't triggered the change event. To make the test, work append the inputElm to the app's `$rootElement`, and the `$rootElement` to the `$document`. After: ```js it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) { var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope); $rootElement.append(inputElm); $document.append($rootElement); inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger() expect($rootScope.checkbox).toBe(true); }); ``` `triggerHandler()`: If you are using this jQuery / jqLite function on the input elements, you don't have to attach the elements to the document, but instead change the triggered event to "change". This is because `triggerHandler(event)` only triggers the exact event when it has been added by jQuery / jqLite.
1 parent 5462373 commit 656c8fa

File tree

6 files changed

+46
-9
lines changed

6 files changed

+46
-9
lines changed

src/ng/directive/input.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1820,7 +1820,7 @@ function radioInputType(scope, element, attr, ctrl) {
18201820
}
18211821
};
18221822

1823-
element.on('click', listener);
1823+
element.on('change', listener);
18241824

18251825
ctrl.$render = function() {
18261826
var value = attr.value;
@@ -1854,7 +1854,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
18541854
ctrl.$setViewValue(element[0].checked, ev && ev.type);
18551855
};
18561856

1857-
element.on('click', listener);
1857+
element.on('change', listener);
18581858

18591859
ctrl.$render = function() {
18601860
element[0].checked = ctrl.$viewValue;

test/BinderSpec.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,17 @@ describe('Binder', function() {
401401
expect(optionC.text()).toEqual('C');
402402
}));
403403

404-
it('ItShouldSelectTheCorrectRadioBox', inject(function($rootScope, $compile) {
404+
it('ItShouldSelectTheCorrectRadioBox', inject(function($rootScope, $compile, $rootElement, $document) {
405405
element = $compile(
406406
'<div>' +
407407
'<input type="radio" ng-model="sex" value="female">' +
408408
'<input type="radio" ng-model="sex" value="male">' +
409409
'</div>')($rootScope);
410+
411+
// Append the app to the document so that "click" on a radio/checkbox triggers "change"
412+
// Support: Chrome, Safari 8, 9
413+
jqLite($document[0].body).append($rootElement.append(element));
414+
410415
var female = jqLite(element[0].childNodes[0]);
411416
var male = jqLite(element[0].childNodes[1]);
412417

test/helpers/testabilityPatch.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ function generateInputCompilerHelper(helper) {
319319
};
320320
});
321321
});
322-
inject(function($compile, $rootScope, $sniffer) {
322+
inject(function($compile, $rootScope, $sniffer, $document, $rootElement) {
323323

324324
helper.compileInput = function(inputHtml, mockValidity, scope) {
325325

@@ -341,6 +341,11 @@ function generateInputCompilerHelper(helper) {
341341
// Compile the lot and return the input element
342342
$compile(helper.formElm)(scope);
343343

344+
$rootElement.append(helper.formElm);
345+
// Append the app to the document so that "click" on a radio/checkbox triggers "change"
346+
// Support: Chrome, Safari 8, 9
347+
jqLite($document[0].body).append($rootElement);
348+
344349
spyOn(scope.form, '$addControl').and.callThrough();
345350
spyOn(scope.form, '$$renameControl').and.callThrough();
346351

test/ng/directive/booleanAttrsSpec.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ describe('boolean attr directives', function() {
4242
}));
4343

4444

45-
it('should not bind checked when ngModel is present', inject(function($rootScope, $compile) {
45+
it('should not bind checked when ngModel is present', inject(function($rootScope, $compile, $document, $rootElement) {
4646
// test for https://github.com/angular/angular.js/issues/10662
4747
element = $compile('<input type="checkbox" ng-model="value" ng-false-value="\'false\'" ' +
4848
'ng-true-value="\'true\'" ng-checked="value" />')($rootScope);
49+
50+
// Append the app to the document so that "click" triggers "change"
51+
// Support: Chrome, Safari 8, 9
52+
jqLite($document[0].body).append($rootElement.append(element));
53+
4954
$rootScope.value = 'true';
5055
$rootScope.$digest();
5156
expect(element[0].checked).toBe(true);

test/ng/directive/inputSpec.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -3974,7 +3974,7 @@ describe('input', function() {
39743974

39753975
describe('radio', function() {
39763976

3977-
it('should update the model', function() {
3977+
they('should update the model on $prop event', ['click', 'change'], function(event) {
39783978
var inputElm = helper.compileInput(
39793979
'<input type="radio" ng-model="color" value="white" />' +
39803980
'<input type="radio" ng-model="color" value="red" />' +
@@ -3990,7 +3990,8 @@ describe('input', function() {
39903990
expect(inputElm[1].checked).toBe(true);
39913991
expect(inputElm[2].checked).toBe(false);
39923992

3993-
browserTrigger(inputElm[2], 'click');
3993+
if (event === 'change') inputElm[2].checked = true;
3994+
browserTrigger(inputElm[2], event);
39943995
expect($rootScope.color).toBe('blue');
39953996
});
39963997

@@ -4092,6 +4093,23 @@ describe('input', function() {
40924093
});
40934094

40944095

4096+
they('should update the model on $prop event', ['click', 'change'], function(event) {
4097+
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" />');
4098+
4099+
expect(inputElm[0].checked).toBe(false);
4100+
4101+
$rootScope.$apply('checkbox = true');
4102+
expect(inputElm[0].checked).toBe(true);
4103+
4104+
$rootScope.$apply('checkbox = false');
4105+
expect(inputElm[0].checked).toBe(false);
4106+
4107+
if (event === 'change') inputElm[0].checked = true;
4108+
browserTrigger(inputElm[0], event);
4109+
expect($rootScope.checkbox).toBe(true);
4110+
});
4111+
4112+
40954113
it('should format booleans', function() {
40964114
var inputElm = helper.compileInput('<input type="checkbox" ng-model="name" />');
40974115

test/ng/directive/ngRepeatSpec.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ describe('ngRepeat', function() {
354354
});
355355

356356

357-
it('should iterate over object with changing primitive property values', function() {
357+
it('should iterate over object with changing primitive property values', inject(function($rootElement, $document) {
358358
// test for issue #933
359359

360360
element = $compile(
@@ -365,6 +365,10 @@ describe('ngRepeat', function() {
365365
'</li>' +
366366
'</ul>')(scope);
367367

368+
// Append the app to the document so that "click" on a radio/checkbox triggers "change"
369+
// Support: Chrome, Safari 8, 9
370+
jqLite($document[0].body).append($rootElement.append(element));
371+
368372
scope.items = {misko: true, shyam: true, zhenbo:true};
369373
scope.$digest();
370374
expect(element.find('li').length).toEqual(3);
@@ -395,7 +399,7 @@ describe('ngRepeat', function() {
395399
expect(element.find('input')[0].checked).toBe(false);
396400
expect(element.find('input')[1].checked).toBe(true);
397401
expect(element.find('input')[2].checked).toBe(true);
398-
});
402+
}));
399403
});
400404

401405
describe('alias as', function() {

0 commit comments

Comments
 (0)