Skip to content

Commit 1c9cabf

Browse files
committed
fix($compile): correctly merge consecutive text nodes on IE11
As explained in angular#11781 and angular#14924, IE11 can (under certain circumstances) break up a text node into multiple consecutive ones, breaking interpolated expressions (e.g. `{{'foo'}}` would become `{{` + `'foo'}}`). To work-around this IE11 bug, angular#11796 introduced extra logic to merge consecutive text nodes (on IE11 only), which relies on the text nodes' having the same `parentNode`. This approach works fine in the common case, where `compileNodes` is called with a live NodeList object, because removing a text node from its parent will automatically update the latter's `.childNodes` NodeList. It falls short though, when calling `compileNodes` with either a jqLite/jQuery collection or an Array. In fails in two ways: 1. If the text nodes do not have a parent at the moment of compiling, there will be no merging. (This happens for example on directives with `$transclude: {...}`.) 2. If the text nodes do have a parent, just removing a text node from its parent does **not** remove it from the collection/array, which means that the merged text nodes will still get compiled and linked (and possibly be displayed in the view). E.g. `['{{text1}}', '{{text2}}', '{{text3}}']` will become `['{{text1}}{{text2}}{{text3}}', '{{text2}}', '{{text3}}']`. -- This commit works around the above problems by: 1. Merging consecutive text nodes in the provided list, even if they have no parent. 2. When merging a txt node, explicitly remove it from the list (unless it is a live, auto-updating list). This can nonetheless have undesirable (albeit rare) side effects by overzealously merging text nodes that are not meant to be merged (see "BREAKING CHANGE" section below). Fixes angular#14924 BREAKING CHANGE: **Note:** Everything described below affects **IE11 only**. Previously, consecutive text nodes would not get merged if they had no parent. They will now, which might have unexpectd side effects in the following cases: 1. Passing an array or jqLite/jQuery collection of parent-less text nodes to `$compile` directly: ```js // Assuming: var textNodes = [ document.createTextNode('{{'), document.createTextNode('"foo"'), document.createTextNode('}}') ]; var compiledNodes = $compile(textNodes)($rootScope); // Before: console.log(compiledNodes.length); // 3 console.log(compiledNodes.text()); // {{'foo'}} // After: console.log(compiledNodes.length); // 1 console.log(compiledNodes.text()); // foo // To get the old behavior, compile each node separately: var textNodes = [ document.createTextNode('{{'), document.createTextNode('"foo"'), document.createTextNode('}}') ]; var compiledNodes = angular.element(textNodes.map(function (node) { return $compile(node)($rootScope)[0]; })); ``` 2. Using multi-slot transclusion with non-consecutive, default-content text nodes (that form interpolated expressions when merged): ```js // Assuming the following component: .compoent('someThing', { template: '<ng-transclude><!-- Default content goes here --></ng-transclude>' transclude: { ignored: 'veryImportantContent' } }) ``` ```html <!-- And assuming the following view: --> <some-thing> {{ <very-important-content>Nooot</very-important-content> 'foo'}} </some-thing> <!-- Before: --> <some-thing> <ng-transclude> {{ <-- Two separate 'foo'}} <-- text nodes </ng-transclude> </some-thing> <!-- After: --> <some-thing> <ng-transclude> foo <-- The text nodes were merged into `{{'foo'}}`, which was then interpolated </ng-transclude> </some-thing> <!-- To (visually) get the old behavior, wrap top-level text-nodes on --> <!-- multi-slot transclusion directives into `<span>`; e.g.: --> <some-thing> <span>{{</span> <very-important-content>Nooot</very-important-content> <span>'foo'}}</span> </some-thing> <!-- Result: --> <some-thing> <ng-transclude> <span>{{</span> <-- Two separate <span>'foo'}}</span> <-- nodes </ng-transclude> </some-thing> ```
1 parent 5fc9933 commit 1c9cabf

File tree

2 files changed

+115
-13
lines changed

2 files changed

+115
-13
lines changed

src/ng/compile.js

+33-12
Original file line numberDiff line numberDiff line change
@@ -1842,12 +1842,40 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18421842
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
18431843
previousCompileContext) {
18441844
var linkFns = [],
1845-
attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
1845+
// `nodeList` can be either an element's `.childNodes` (live NodeList)
1846+
// or a jqLite/jQuery collection or an array
1847+
notLiveList = isArray(nodeList) || (nodeList instanceof jqLite),
1848+
attrs, directives, node, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
1849+
18461850

18471851
for (var i = 0; i < nodeList.length; i++) {
18481852
attrs = new Attributes();
1853+
node = nodeList[i];
1854+
1855+
// Workaround for #11781 and #14924
1856+
if ((msie === 11) && (node.nodeType === NODE_TYPE_TEXT)) {
1857+
var parent = node.parentNode;
1858+
var sibling;
1859+
1860+
while (true) {
1861+
sibling = parent ? node.nextSibling : nodeList[i + 1];
1862+
if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) {
1863+
break;
1864+
}
18491865

1850-
// we must always refer to nodeList[i] since the nodes can be replaced underneath us.
1866+
node.nodeValue = node.nodeValue + sibling.nodeValue;
1867+
1868+
if (sibling.parentNode) {
1869+
sibling.parentNode.removeChild(sibling);
1870+
}
1871+
if (notLiveList && sibling === nodeList[i + 1]) {
1872+
nodeList.splice(i + 1, 1);
1873+
}
1874+
}
1875+
}
1876+
1877+
// We must always refer to `nodeList[i]` hereafter,
1878+
// since the nodes can be replaced underneath us.
18511879
directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
18521880
ignoreDirective);
18531881

@@ -2046,13 +2074,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
20462074
}
20472075
break;
20482076
case NODE_TYPE_TEXT: /* Text Node */
2049-
if (msie === 11) {
2050-
// Workaround for #11781
2051-
while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
2052-
node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
2053-
node.parentNode.removeChild(node.nextSibling);
2054-
}
2055-
}
20562077
addTextInterpolateDirective(directives, node.nodeValue);
20572078
break;
20582079
case NODE_TYPE_COMMENT: /* Comment */
@@ -2325,9 +2346,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
23252346

23262347
var slots = createMap();
23272348

2328-
$template = jqLite(jqLiteClone(compileNode)).contents();
2329-
2330-
if (isObject(directiveValue)) {
2349+
if (!isObject(directiveValue)) {
2350+
$template = jqLite(jqLiteClone(compileNode)).contents();
2351+
} else {
23312352

23322353
// We have transclusion slots,
23332354
// collect them up, compile them and store their transclusion functions

test/ng/compileSpec.js

+82-1
Original file line numberDiff line numberDiff line change
@@ -3260,6 +3260,26 @@ describe('$compile', function() {
32603260
}));
32613261

32623262

3263+
it('should not process text nodes merged into their sibling', inject(function($compile, $rootScope) {
3264+
var div = document.createElement('div');
3265+
div.appendChild(document.createTextNode('1{{ value }}'));
3266+
div.appendChild(document.createTextNode('2{{ value }}'));
3267+
div.appendChild(document.createTextNode('3{{ value }}'));
3268+
3269+
element = jqLite(div.childNodes);
3270+
3271+
var initialWatcherCount = $rootScope.$countWatchers();
3272+
$compile(element)($rootScope);
3273+
$rootScope.$apply('value = 0');
3274+
var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount;
3275+
3276+
expect(element.text()).toBe('102030');
3277+
expect(newWatcherCount).toBe(3);
3278+
3279+
dealoc(div);
3280+
}));
3281+
3282+
32633283
it('should support custom start/end interpolation symbols in template and directive template',
32643284
function() {
32653285
module(function($interpolateProvider, $compileProvider) {
@@ -8530,10 +8550,38 @@ describe('$compile', function() {
85308550
element = $compile('<div transclude><div child></div></div>')($rootScope);
85318551
expect(capturedChildCtrl).toBeTruthy();
85328552
});
8533-
85348553
});
85358554

85368555

8556+
// See issue https://github.com/angular/angular.js/issues/14924
8557+
it('should not process top-level transcluded text nodes merged into their sibling',
8558+
function() {
8559+
module(function() {
8560+
directive('transclude', valueFn({
8561+
template: '<ng-transclude></ng-transclude>',
8562+
transclude: true,
8563+
scope: {}
8564+
}));
8565+
});
8566+
8567+
inject(function($compile) {
8568+
element = jqLite('<div transclude></div>');
8569+
element[0].appendChild(document.createTextNode('1{{ value }}'));
8570+
element[0].appendChild(document.createTextNode('2{{ value }}'));
8571+
element[0].appendChild(document.createTextNode('3{{ value }}'));
8572+
8573+
var initialWatcherCount = $rootScope.$countWatchers();
8574+
$compile(element)($rootScope);
8575+
$rootScope.$apply('value = 0');
8576+
var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount;
8577+
8578+
expect(element.text()).toBe('102030');
8579+
expect(newWatcherCount).toBe(3);
8580+
});
8581+
}
8582+
);
8583+
8584+
85378585
// see issue https://github.com/angular/angular.js/issues/9413
85388586
describe('passing a parent bound transclude function to the link ' +
85398587
'function returned from `$compile`', function() {
@@ -9996,6 +10044,39 @@ describe('$compile', function() {
999610044
expect(element.children().eq(2).text()).toEqual('dorothy');
999710045
});
999810046
});
10047+
10048+
10049+
// See issue https://github.com/angular/angular.js/issues/14924
10050+
it('should not process top-level transcluded text nodes merged into their sibling',
10051+
function() {
10052+
module(function() {
10053+
directive('transclude', valueFn({
10054+
template: '<ng-transclude></ng-transclude>',
10055+
transclude: {},
10056+
scope: {}
10057+
}));
10058+
});
10059+
10060+
inject(function($compile) {
10061+
element = jqLite('<div transclude></div>');
10062+
element[0].appendChild(document.createTextNode('1{{ value }}'));
10063+
element[0].appendChild(document.createTextNode('2{{ value }}'));
10064+
element[0].appendChild(document.createTextNode('3{{ value }}'));
10065+
10066+
var initialWatcherCount = $rootScope.$countWatchers();
10067+
$compile(element)($rootScope);
10068+
$rootScope.$apply('value = 0');
10069+
var newWatcherCount = $rootScope.$countWatchers() - initialWatcherCount;
10070+
10071+
expect(element.text()).toBe('102030');
10072+
expect(newWatcherCount).toBe(3);
10073+
10074+
if (msie === 11) {
10075+
expect(element.find('ng-transclude').contents().length).toBe(1);
10076+
}
10077+
});
10078+
}
10079+
);
999910080
});
1000010081

1000110082

0 commit comments

Comments
 (0)