Skip to content

Commit 61767da

Browse files
committed
Workaround bindings to textarea.placeholder in IE
Textareas have a very weird bug in IE, where the text of the placeholder is copied to the content of the textarea when set. This a creates two bugs: 1. An unintended binding is made to the textContent of the textarea's text child node, meaning updates to the `placeholder` will be an unnecessary binding process in the best case, or an exception thrown when updating the text child node in the worst case. 2. When `legacyOptimizations` is enabled, the child node of the text area is removed when the binding for `placeholder` is processed and removed, leaving a binding to a `null` node, and throwing exceptions. Therefore, when we detect this placeholder behavior, we will remove the textnode before template processing, preventing both bugs.
1 parent 69ee468 commit 61767da

File tree

2 files changed

+83
-3
lines changed

2 files changed

+83
-3
lines changed

lib/mixins/template-stamp.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,49 @@ const templateExtensions = {
2222
'dom-if': true,
2323
'dom-repeat': true
2424
};
25+
26+
let placeholderBugDetect = false;
27+
let placeholderBug = false;
28+
29+
function hasPlaceholderBug() {
30+
if (!placeholderBugDetect) {
31+
const t = document.createElement('textarea');
32+
t.placeholder = 'a';
33+
placeholderBug = t.placeholder === t.textContent;
34+
}
35+
return placeholderBug;
36+
}
37+
38+
/**
39+
* Some browsers have a bug with textarea, where placeholder text is copied as
40+
* a textnode child of the textarea.
41+
*
42+
* If the placeholder is a binding, this can break template stamping in two
43+
* ways.
44+
*
45+
* One issue is that when the `placeholder` binding is removed, the textnode
46+
* child of the textarea is deleted, and the template info tries to bind into
47+
* that node.
48+
*
49+
* When `legacyOptimizations` is enabled, the node is removed from the textarea
50+
* when the `placeholder` binding is processed, leaving an "undefined" cell in
51+
* the binding metadata object.
52+
*
53+
* When `legacyOptimizations` is disabled, the template is cloned before
54+
* processing, and has an extra binding to the textContent of the text node
55+
* child of the textarea. This at best is an extra binding to process that has
56+
* no useful effect, and at worst throws exceptions trying to update the text
57+
* node.
58+
*
59+
* @param {!Node} node Check node for placeholder bug
60+
* @return {boolean} True if placeholder is bugged
61+
*/
62+
function shouldFixPlaceholder(node) {
63+
return hasPlaceholderBug()
64+
&& node.localName === 'textarea' && node.placeholder
65+
&& node.placeholder === node.textContent;
66+
}
67+
2568
function wrapTemplateExtension(node) {
2669
let is = node.getAttribute('is');
2770
if (is && templateExtensions[is]) {
@@ -251,6 +294,9 @@ export const TemplateStamp = dedupingMixin(
251294
// For ShadyDom optimization, indicating there is an insertion point
252295
templateInfo.hasInsertionPoint = true;
253296
}
297+
if (shouldFixPlaceholder(node)) {
298+
node.textContent = null;
299+
}
254300
if (element.firstChild) {
255301
this._parseTemplateChildNodes(element, templateInfo, nodeInfo);
256302
}

test/unit/property-effects-template.html

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
<meta charset="utf-8">
1414
<script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
1515
<script src="wct-browser-config.js"></script>
16-
<script src="wct-browser-config.js"></script>
1716
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
1817
<script type="module" src="../../polymer-element.js"></script>
1918
<script type="module" src="../../lib/mixins/gesture-event-listeners.js"></script>
@@ -282,9 +281,10 @@
282281
</dom-module>
283282

284283
<script type="module">
285-
import '../../polymer-element.js';
284+
import {PolymerElement, html} from '../../polymer-element.js';
286285
import '../../lib/mixins/gesture-event-listeners.js';
287286
import '../../lib/elements/dom-if.js';
287+
import {setLegacyOptimizations} from '../../lib/utils/settings.js';
288288

289289
suite('runtime template stamping', function() {
290290

@@ -694,7 +694,6 @@
694694
'x-runtime'
695695
]);
696696
});
697-
698697
});
699698

700699
suite('template parsing hooks', () => {
@@ -818,6 +817,41 @@
818817
document.body.removeChild(el);
819818
});
820819
});
820+
821+
suite('textarea placeholder bug', function() {
822+
class PlaceholderBase extends PolymerElement {
823+
static get template() {
824+
return html`<textarea id="textarea" placeholder="[[value]]"></textarea>`;
825+
}
826+
static get properties() {
827+
return {value: {type: String}};
828+
}
829+
}
830+
test('placeholder binding does not leak to textContent', function() {
831+
customElements.define('placeholder-duplicate', class extends PlaceholderBase {});
832+
const el = document.createElement('placeholder-duplicate');
833+
document.body.appendChild(el);
834+
const textarea = el.$.textarea;
835+
el.value = 'before';
836+
textarea.value = 'Hello!';
837+
el.value = 'after';
838+
assert.equal(textarea.value, 'Hello!');
839+
});
840+
suite('legacyOptimizations', function() {
841+
suiteSetup(function() {
842+
setLegacyOptimizations(true);
843+
});
844+
suiteTeardown(function() {
845+
setLegacyOptimizations(false);
846+
});
847+
test('textarea placeholder binding works with legacyOptimizations', function() {
848+
customElements.define('placeholder-bug', class extends PlaceholderBase {});
849+
const el = document.createElement('placeholder-bug');
850+
document.body.appendChild(el);
851+
assert.doesNotThrow(() => {el.value = 'bar';});
852+
});
853+
});
854+
});
821855
</script>
822856

823857
</body>

0 commit comments

Comments
 (0)