Skip to content

Commit e1b9f59

Browse files
committed
fix(input): listen on "change" for radio and checkbox
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 angular#4516 Closes angular#14667 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: - 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 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 have 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. - Custom click events: Before this change, custom click handlers 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() and event.stopImmediatePropagation() to prevent the input from updating. If an app relied on the model being updated when the custom click handler has been called, you should register a "change" handler instead.
1 parent 1e841a8 commit e1b9f59

File tree

3 files changed

+26
-5
lines changed

3 files changed

+26
-5
lines changed

src/ng/directive/input.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1816,7 +1816,7 @@ function radioInputType(scope, element, attr, ctrl) {
18161816
}
18171817
};
18181818

1819-
element.on('click', listener);
1819+
element.on('change', listener);
18201820

18211821
ctrl.$render = function() {
18221822
var value = attr.value;
@@ -1850,7 +1850,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
18501850
ctrl.$setViewValue(element[0].checked, ev && ev.type);
18511851
};
18521852

1853-
element.on('click', listener);
1853+
element.on('change', listener);
18541854

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

test/helpers/testabilityPatch.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ function generateInputCompilerHelper(helper) {
359359
};
360360
});
361361
});
362-
inject(function($compile, $rootScope, $sniffer) {
362+
inject(function($compile, $rootScope, $sniffer, $document, $rootElement) {
363363

364364
helper.compileInput = function(inputHtml, mockValidity, scope) {
365365

@@ -381,6 +381,9 @@ function generateInputCompilerHelper(helper) {
381381
// Compile the lot and return the input element
382382
$compile(helper.formElm)(scope);
383383

384+
$rootElement.append(helper.formElm);
385+
jqLite($document[0].body).append($rootElement);
386+
384387
spyOn(scope.form, '$addControl').and.callThrough();
385388
spyOn(scope.form, '$$renameControl').and.callThrough();
386389

test/ng/directive/inputSpec.js

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

39633963
describe('radio', function() {
39643964

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

3981-
browserTrigger(inputElm[2], 'click');
3981+
if (event === 'change') inputElm[2].checked = true;
3982+
browserTrigger(inputElm[2], event);
39823983
expect($rootScope.color).toBe('blue');
39833984
});
39843985

@@ -4080,6 +4081,23 @@ describe('input', function() {
40804081
});
40814082

40824083

4084+
they('should update the model on $prop event', ['click', 'change'], function(event) {
4085+
var inputElm = helper.compileInput('<input type="checkbox" ng-model="checkbox" />');
4086+
4087+
expect(inputElm[0].checked).toBe(false);
4088+
4089+
$rootScope.$apply("checkbox = true");
4090+
expect(inputElm[0].checked).toBe(true);
4091+
4092+
$rootScope.$apply("checkbox = false");
4093+
expect(inputElm[0].checked).toBe(false);
4094+
4095+
if (event === 'change') inputElm[0].checked = true;
4096+
browserTrigger(inputElm[0], event);
4097+
expect($rootScope.checkbox).toBe(true);
4098+
});
4099+
4100+
40834101
it('should format booleans', function() {
40844102
var inputElm = helper.compileInput('<input type="checkbox" ng-model="name" />');
40854103

0 commit comments

Comments
 (0)