diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 725e2877078f..760fa835bd48 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -7,7 +7,7 @@ htmlAnchorDirective, inputDirective, - inputDirective, + hiddenInputBrowserCacheDirective, formDirective, scriptDirective, selectDirective, @@ -221,7 +221,8 @@ function publishExternalAPI(angular) { ngModelOptions: ngModelOptionsDirective }). directive({ - ngInclude: ngIncludeFillContentDirective + ngInclude: ngIncludeFillContentDirective, + input: hiddenInputBrowserCacheDirective }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index bf6576f81a09..2f75defe1944 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -2193,6 +2193,48 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', }]; +var hiddenInputBrowserCacheDirective = function() { + var valueProperty = { + configurable: true, + enumerable: false, + get: function() { + return this.getAttribute('value') || ''; + }, + set: function(val) { + this.setAttribute('value', val); + } + }; + + return { + restrict: 'E', + priority: 200, + compile: function(_, attr) { + if (lowercase(attr.type) !== 'hidden') { + return; + } + + return { + pre: function(scope, element, attr, ctrls) { + var node = element[0]; + + // Support: Edge + // Moving the DOM around prevents autofillling + if (node.parentNode) { + node.parentNode.insertBefore(node, node.nextSibling); + } + + // Support: FF, IE + // Avoiding direct assignment to .value prevents autofillling + if (Object.defineProperty) { + Object.defineProperty(node, 'value', valueProperty); + } + } + }; + } + }; +}; + + var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; /** diff --git a/test/e2e/fixtures/back2dom/index.html b/test/e2e/fixtures/back2dom/index.html new file mode 100644 index 000000000000..6134b796a1fa --- /dev/null +++ b/test/e2e/fixtures/back2dom/index.html @@ -0,0 +1,13 @@ + + +
+ + + + + \ No newline at end of file diff --git a/test/e2e/fixtures/back2dom/script.js b/test/e2e/fixtures/back2dom/script.js new file mode 100644 index 000000000000..04911865c39d --- /dev/null +++ b/test/e2e/fixtures/back2dom/script.js @@ -0,0 +1,11 @@ +'use strict'; + +angular + .module('test', []) + .run(function($rootScope) { + $rootScope.internalFnCalled = false; + + $rootScope.internalFn = function() { + $rootScope.internalFnCalled = true; + }; + }); diff --git a/test/e2e/tests/input-hidden.spec.js b/test/e2e/tests/input-hidden.spec.js index ef2669f0f64a..e1e76e0390a4 100644 --- a/test/e2e/tests/input-hidden.spec.js +++ b/test/e2e/tests/input-hidden.spec.js @@ -14,4 +14,72 @@ describe('hidden thingy', function() { var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : ''; expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue); }); + + it('should prevent browser autofill on browser.refresh', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.refresh(); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on location.reload', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.driver.executeScript('location.reload()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on history.back', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + loadFixture('sample'); + + browser.driver.executeScript('history.back()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on history.forward', function() { + + loadFixture('sample'); + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.driver.executeScript('history.back()'); + browser.driver.executeScript('history.forward()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); });