-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($compile): workaround for IE11 MutationObserver #11796
Conversation
Does this only happen on IE11 or other versions of IE? |
@petebacondarwin AFAICT, this is IE11 only and when there is a MutationObserver |
BTW, the test as is fails on all browsers. The main issue is that only IE11 generates multiple consecutive text elements/ |
it('should handle consecutive text elements as a single text element', inject(function($rootScope, $compile) { | ||
var base = jqLite('<div></div>'); | ||
base[0].appendChild(document.createTextNode('Lucas {{1+')); | ||
base[0].appendChild(document.createTextNode('2}} Lucas')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should test the specific situation that causes the bug, i.e. that there is a mutation observer and that the text contains some special characters that trigger IE11 to split it into multiple text nodes.
b286e3e
to
cfef162
Compare
@petebacondarwin can you take a second look at this? |
@@ -2762,6 +2762,23 @@ describe('$compile', function() { | |||
})); | |||
|
|||
|
|||
it('should handle consecutive text elements as a single text element', inject(function($rootScope, $compile) { | |||
window.addMutationObserver = function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we really be modifying the window
object here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this have any effect anyway ? Shouldn't we call the function as well ?
(And indeed there doesn't seem to be a reason to modify window
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test, without fiddling with window
passes without the fix on IE 11.
IE11 parses text node differently when there is a mutation observer, this is why the test add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if the function isn't called, then there is no MutationObserver added right ?
How does the function help if we don't call it ?
And why does the function need to be assigned on window ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gkalpak I think I am missing something. This is a test. IE11 parses html different when there is any mutation observer registered. The test reproduces this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lgalfaso, if I recall correctly, the issue appeared only when there was a childList+subtree
MutationObserver registered on the page. I would expect a test that calls new MutationObserver(...).observe(...)
.
Instead I see:
window.addMutationObserver = function () {
// A function that _would_ register a MutationObserver on body
// IF it where executed.
};
// Then I see the actual test code being executed without ever calling `window.addMutationObserver()`
// which would have registered the MutationObserver. AFAICT, the MutationObserver is never created.
Based on the above, there doesn't seem to be any MutationObserver during this test, so it would pass even without the fix (I am just speculating - I haven't actually tried it).
I would expect something along these lines:
it ('...', inject(function($compile, $rootScope) {
// No point it running the test, if there is no MutationObserver
if (!window.MutationObserver) return;
// Create and register the MutationObserver
var observer = new window.MutationObserver(noop);
observer.observe(document.body, {childList: true, sublist: true});
// Run the actual test
var base = jqLite('<div>— {{ "This doesn\'t." }}</div>');
element = $compile(base)($rootScope);
$rootScope.$digest();
expect(element.text()).toBe("— This doesn't.");
// Unregister the MutationObserver (and hope it doesn't mess up with subsequent tests)
observer.disconnect();
}));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than my comment about |
IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Closes angular#11781
landed as f3b1d0b |
Backport angular#11796 to 1.2 branch. IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Closes angular#11781
Backport angular#11796 to 1.2 branch. IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Closes angular#11781
Backport angular#11796 to 1.2 branch. IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Closes angular#11781
Backport angular#11796 to 1.2 branch. IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Also had to modify npm-shrinkwrap.json because [email protected] was unpublished from npm. Closes angular#11781
Backport #11796 to 1.2 branch. IE11 MutationObserver breaks consecutive text nodes into several text nodes. This patch merges consecutive text nodes into a single node before looking for interpolations. Also had to modify npm-shrinkwrap.json because [email protected] was unpublished from npm. Closes #11781 Closes #12613
Related to angular#11796. Fixes angular#14924 BREAKING CHANGE: // TODO
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> ```
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> ```
As explained in #11781 and #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, #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 text 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 the "BREAKING CHANGE" section below). Fixes #14924 Closes #15025 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>` elements; 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> ```
IE11 MutationObserver breaks consecutive text nodes into several text nodes.
This patch merges consecutive text nodes into a single node before
looking for interpolations.
Closes #11781