diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 9644ab21a0d7..59650db3d507 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -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,
@@ -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
@@ -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);
+ }
});
}
};
@@ -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);
});
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js
index 2ad2666df50a..bbd02f28a9fe 100644
--- a/src/ng/sniffer.js
+++ b/src/ng/sniffer.js
@@ -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');
diff --git a/src/ngScenario/dsl.js b/src/ngScenario/dsl.js
index c6d7bc6844e4..c1ed8addb789 100644
--- a/src/ngScenario/dsl.js
+++ b/src/ngScenario/dsl.js
@@ -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 + "'",
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index e579eb0f8db3..9fe5d97f656e 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -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('');
- 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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('');
+ 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() {
@@ -1656,7 +1804,7 @@ 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) {
@@ -1664,9 +1812,13 @@ describe('input', function() {
};
});
- it('should update the model on "paste" event', function() {
+ it('should update the model on "paste" event if the input value changes', function() {
compileInput('');
+ browserTrigger(inputElm, 'keydown');
+ $browser.defer.flush();
+ expect(inputElm).toBePristine();
+
inputElm.val('mark');
browserTrigger(inputElm, 'paste');
$browser.defer.flush();
@@ -1682,6 +1834,21 @@ describe('input', function() {
expect(scope.name).toEqual('john');
});
+ it('should cancel the delayed dirty if a change occurs', function() {
+ compileInput('');
+ 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();
+ });
});
diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js
index 7d48bc79ffea..382952607686 100644
--- a/test/ng/snifferSpec.js
+++ b/test/ng/snifferSpec.js
@@ -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));
});
});