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

fix(inputs): ignoring input events in IE caused by placeholder changes o... #9265

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}

function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var placeholder = element[0].placeholder, noevent = {};
var type = lowercase(element[0].type);

// In composition mode, users are still inputing intermediate text buffer,
Expand All @@ -974,19 +973,14 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}

var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
timeout = null;
}
if (composing) return;
var value = element.val(),
event = ev && ev.type;

// IE (11 and under) seem to emit an 'input' event if the placeholder value changes.
// We don't want to dirty the value when this happens, so we abort here. Unfortunately,
// IE also sends input events for other non-input-related things, (such as focusing on a
// form control), so this change is not entirely enough to solve this.
if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) {
placeholder = element[0].placeholder;
return;
}

// By default we will trim the value
// If the attribute ng-trim exists we will avoid trimming
// If input type is 'password', the value is never trimmed
Expand All @@ -1009,11 +1003,13 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
} else {
var timeout;

var deferListener = function(ev) {
var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
listener(ev);
timeout = null;
if (!input || input.value !== origValue) {
listener(ev);
}
});
}
};
Expand All @@ -1025,7 +1021,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// command modifiers arrows
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;

deferListener(event);
deferListener(event, this, this.value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use this here? We are in an event handler. The unit tests do not appear call this line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I take that back - they do get called when running on IE.

});

// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
Expand Down
4 changes: 3 additions & 1 deletion src/ng/sniffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ function $SnifferProvider() {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
// it. In particular the event is not fired when backspace or delete key are pressed or
// when cut operation is performed.
if (event == 'input' && msie == 9) return false;
// IE10+ implements 'input' event but it erroneously fires under various situations,
// e.g. when placeholder changes, or a form is focused.
if (event === 'input' && msie <= 11) return false;

if (isUndefined(eventSupport[event])) {
var divElm = document.createElement('div');
Expand Down
2 changes: 1 addition & 1 deletion src/ngScenario/dsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ angular.scenario.dsl('binding', function() {
*/
angular.scenario.dsl('input', function() {
var chain = {};
var supportInputEvent = 'oninput' in document.createElement('div') && msie != 9;
var supportInputEvent = 'oninput' in document.createElement('div') && !(msie && msie <= 11);

chain.enter = function(value, event) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'",
Expand Down
191 changes: 179 additions & 12 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1549,22 +1549,170 @@ describe('input', function() {
expect(scope.name).toEqual('caitp');
});

it('should not dirty the model on an input event in response to a placeholder change', inject(function($sniffer) {
if (msie && $sniffer.hasEvent('input')) {
compileInput('<input type="text" ng-model="name" name="name" />');
inputElm.attr('placeholder', 'Test');
browserTrigger(inputElm, 'input');

describe("IE placeholder input events", function() {
//IE fires an input event whenever a placeholder visually changes, essentially treating it as a value
//Events:
// placeholder attribute change: *input*
// focus (which visually removes the placeholder value): focusin focus *input*
// blur (which visually creates the placeholder value): focusout *input* blur
//However none of these occur if the placeholder is not visible at the time of the event.
//These tests try simulate various scenerios which do/do-not fire the extra input event

it('should not dirty the model on an input event in response to a placeholder change', function() {
compileInput('<input type="text" placeholder="Test" attr-capture ng-model="unsetValue" name="name" />');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test');
expect(inputElm).toBePristine();

inputElm.attr('placeholder', 'Test Again');
browserTrigger(inputElm, 'input');
attrs.$set('placeholder', '');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('');
expect(inputElm).toBePristine();

attrs.$set('placeholder', 'Test Again');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test Again');
expect(inputElm).toBePristine();
}
}));

attrs.$set('placeholder', undefined);
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe(undefined);
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
});

it('should not dirty the model on an input event in response to a interpolated placeholder change', inject(function($rootScope) {
compileInput('<input type="text" placeholder="{{ph}}" ng-model="unsetValue" name="name" />');
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

$rootScope.ph = 1;
$rootScope.$digest();
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

$rootScope.ph = "";
$rootScope.$digest();
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
}));

it('should dirty the model on an input event while in focus even if the placeholder changes', inject(function($rootScope) {
$rootScope.ph = 'Test';
compileInput('<input type="text" ng-attr-placeholder="{{ph}}" ng-model="unsetValue" name="name" />');
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusin');
browserTrigger(inputElm, 'focus');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test');
expect(inputElm).toBePristine();

$rootScope.ph = 'Test Again';
$rootScope.$digest();
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
}));

it('should not dirty the model on an input event in response to a ng-attr-placeholder change', inject(function($rootScope) {
compileInput('<input type="text" ng-attr-placeholder="{{ph}}" ng-model="unsetValue" name="name" />');
expect(inputElm).toBePristine();

$rootScope.ph = 1;
$rootScope.$digest();
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

$rootScope.ph = "";
$rootScope.$digest();
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
}));

it('should not dirty the model on an input event in response to a focus', inject(function($sniffer) {
compileInput('<input type="text" placeholder="Test" ng-model="unsetValue" name="name" />');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test');
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusin');
browserTrigger(inputElm, 'focus');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test');
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
}));

it('should not dirty the model on an input event in response to a blur', inject(function($sniffer) {
compileInput('<input type="text" placeholder="Test" ng-model="unsetValue" name="name" />');
msie && browserTrigger(inputElm, 'input');
expect(inputElm.attr('placeholder')).toBe('Test');
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusin');
browserTrigger(inputElm, 'focus');
msie && browserTrigger(inputElm, 'input');
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusout');
msie && browserTrigger(inputElm, 'input');
browserTrigger(inputElm, 'blur');
expect(inputElm).toBePristine();

changeInputValueTo('foo');
expect(inputElm).toBeDirty();
}));

it('should dirty the model on an input event if there is a placeholder and value', inject(function($rootScope) {
$rootScope.name = 'foo';
compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />');
expect(inputElm.val()).toBe($rootScope.name);
expect(inputElm).toBePristine();

changeInputValueTo('bar');
expect(inputElm).toBeDirty();
}));

it('should dirty the model on an input event if there is a placeholder and value after focusing', inject(function($rootScope) {
$rootScope.name = 'foo';
compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />');
expect(inputElm.val()).toBe($rootScope.name);
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusin');
browserTrigger(inputElm, 'focus');
changeInputValueTo('bar');
expect(inputElm).toBeDirty();
}));

it('should dirty the model on an input event if there is a placeholder and value after bluring', inject(function($rootScope) {
$rootScope.name = 'foo';
compileInput('<input type="text" placeholder="Test" ng-model="name" value="init" name="name" />');
expect(inputElm.val()).toBe($rootScope.name);
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusin');
browserTrigger(inputElm, 'focus');
expect(inputElm).toBePristine();

browserTrigger(inputElm, 'focusout');
browserTrigger(inputElm, 'blur');
changeInputValueTo('bar');
expect(inputElm).toBeDirty();
}));
});


it('should interpolate input names', function() {
Expand Down Expand Up @@ -1656,17 +1804,21 @@ describe('input', function() {
}
});

describe('"paste" and "cut" events', function() {
describe('"keydown", "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() {
it('should update the model on "paste" event if the input value changes', function() {
compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />');

browserTrigger(inputElm, 'keydown');
$browser.defer.flush();
expect(inputElm).toBePristine();

inputElm.val('mark');
browserTrigger(inputElm, 'paste');
$browser.defer.flush();
Expand All @@ -1682,6 +1834,21 @@ describe('input', function() {
expect(scope.name).toEqual('john');
});

it('should cancel the delayed dirty if a change occurs', function() {
compileInput('<input type="text" ng-model="name" />');
var ctrl = inputElm.controller('ngModel');

browserTrigger(inputElm, 'keydown', {target: inputElm[0]});
inputElm.val('f');
browserTrigger(inputElm, 'change');
expect(inputElm).toBeDirty();

ctrl.$setPristine();
scope.$apply();

$browser.defer.flush();
expect(inputElm).toBePristine();
});
});


Expand Down
3 changes: 2 additions & 1 deletion test/ng/snifferSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ describe('$sniffer', function() {

it('should claim that IE9 doesn\'t have support for "oninput"', function() {
// IE9 implementation is fubared, so it's better to pretend that it doesn't have the support
// IE10+ implementation is fubared when mixed with placeholders
mockDivElement = {oninput: noop};

expect($sniffer.hasEvent('input')).toBe((msie == 9) ? false : true);
expect($sniffer.hasEvent('input')).toBe(!(msie && msie <= 11));
});
});

Expand Down