Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Interpolations containing HTML entities fail in IE11 when MutationObservers are active #11781

Closed
zachsnow opened this issue May 1, 2015 · 24 comments

Comments

@zachsnow
Copy link
Contributor

zachsnow commented May 1, 2015

Text nodes that contain unicode / HTML entities are not properly interpolated in IE11 when there is a MutationObserver active. Consider the following example.

<!doctype html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-rc.1/angular.min.js"></script>
    <script>
      window.addMutationObserver = function(){
        if(!window.MutationObserver){
          return;
        }
        new window.MutationObserver(function(){}).observe(document.body, {
          childList: true,
          subtree: true
        });
      };

      angular.module('example', []);
    </script>
  </head>
  <body onload="addMutationObserver()" ng-app="example">
    <span>{{ 'This works.' }}</span>
    <span>&mdash; {{ "This doesn't." }}</span>
  </body>
</html>

The output is:

This works. — {{ "This doesn't." }}

If you remove the MutationObserver the code works as expected. If you don't set subtree: true the code works as expected. If you remove the entity it also works as expected. If instead you replace the entity with the actual character (—) it remains broken. It's also broken in 1.3.

We recently started seeing this bug because Wordpress has some Emoji polyfills that use MutationObserver. This issue breaks interpolation even when the Angular application is running in an iframe and the code that is attaching the MutationObserver is in the parent frame, which seems pretty crazy. Because of this we seek a workaround on the Angular side, as we can't change every site that might embed Angular in an iframe -- perhaps this is a bug in IE.

@zachsnow
Copy link
Contributor Author

zachsnow commented May 1, 2015

Using ng-bind in combination with additional nodes can work around the issue -- it's not always sufficient to wrap the interpolations in, say, <span /> elements.

@lgalfaso
Copy link
Contributor

lgalfaso commented May 1, 2015

This looks like a real issue, unlucky I do not have access to an IE11 so to diagnose this I will need some help

@gkalpak
Copy link
Member

gkalpak commented May 2, 2015

Took a brief look and here is what is going on:

// Given the following HTML:
<span>&mdash; {{ 'foo' }}</span>
var span = /* above <span> */

// In Chrome, IE11 (without MutationObserver):
span.childNodes = ["— foo"];   // All `textContent` is contained in 1 TextNode

// In IE11 (with MutationObserver):
span.childNodes = ["— ", "{{ '", "foo' }}"];   // The `textContent` is broken up into 3 TextNodes

So, in the former case, $iterpolate is called once with — {{ 'foo' }}, whereas in the latter it is called 3 times with , {{ ' and foo' }} as arguments respectively.

(I still don't know why this happens and if Angular can do something about it.)

@lgalfaso, I do have access to IE11; let me know if I can be of any help.

@lgalfaso
Copy link
Contributor

lgalfaso commented May 2, 2015

@gkalpak is this also true if, after creating the node template out of the string template, we call node.normalize() ?

@lgalfaso
Copy link
Contributor

lgalfaso commented May 2, 2015

@gkalpak or even simpler (even when not better), calling node.normalize() for every node that gets into $compile

@gkalpak
Copy link
Member

gkalpak commented May 2, 2015

@lgalfaso, will try it and get back to you (here or elsewhere).

BTW, I've been using this CodePen to reproduce the issue (should anyone be interested).

@lgalfaso
Copy link
Contributor

lgalfaso commented May 3, 2015

The first part is clear, this is an issue with IE11 as text elements must not be split. Now, if calling normalize would work as expected, then I think this would be a workaround that can be added to Angular. As @gkalpak found out, IE11 normalize is buggy too.
https://connect.microsoft.com/IE/feedback/details/809424

This implies that any solution will not end up being pretty. All that said, I think that there is nothing for angular to do to workaround the IE11 bug, but I will leave this issue open for people to comment and if someone wants to create a IE11 specific workaround, then to post it here.

@gkalpak
Copy link
Member

gkalpak commented May 3, 2015

IE11 + MutationObserver does some weird stuff after encountering the interpolation symbol ({{) and other special characters (e.g. [[) after an HTML entity.

One possible work-around is to use non-special characters as interpolation symbols. I am not sure what qualifies as "non-special", but alphabetic characters do. So, for example, using aa as the interolation start symbol (via $interpolateProvider.startSymbol('aa')) will work.
(But it's such an ugly work-around 😞)

lgalfaso added a commit to lgalfaso/angular.js that referenced this issue May 3, 2015
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
@lgalfaso
Copy link
Contributor

lgalfaso commented May 3, 2015

@gkalpak can you check if IE11 is happy with #11796 ?

@gkalpak
Copy link
Member

gkalpak commented May 4, 2015

@lgalfaso, IE11 is indeed happy with #11796.

@lgalfaso
Copy link
Contributor

lgalfaso commented May 4, 2015

@petebacondarwin
Copy link
Contributor

@lgalfaso: I commented on the PR

@petebacondarwin petebacondarwin added this to the 1.4.x - jaracimrman-existence milestone May 5, 2015
lgalfaso added a commit to lgalfaso/angular.js that referenced this issue May 10, 2015
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
lgalfaso added a commit to lgalfaso/angular.js that referenced this issue May 10, 2015
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
lgalfaso added a commit to lgalfaso/angular.js that referenced this issue Jun 6, 2015
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
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 9, 2015
The fixed test is supposed to test a fix for an IE11 bug/peculiarity that
arises when using a specifically configured MutationObserver on the page
(see angular#11781 for more info).
The configuration contained a typo (`sublist` instead of `subtree`), which
effectively failed to set up the MutationObserver in a way that would make
the IE11 bug appear.
gkalpak added a commit to gkalpak/angular.js that referenced this issue Jun 9, 2015
The fixed test is supposed to test a fix for an IE11 bug/peculiarity that
arises when using a specifically configured MutationObserver on the page
(see angular#11781 for more info).
The configuration contained a typo (`sublist` instead of `subtree`), which
effectively failed to set up the MutationObserver in a way that would make
the IE11 bug appear.

Closes angular#12061
gkalpak added a commit that referenced this issue Jun 9, 2015
The fixed test is supposed to test a fix for an IE11 bug/peculiarity that
arises when using a specifically configured MutationObserver on the page
(see #11781 for more info).
The configuration contained a typo (`sublist` instead of `subtree`), which
effectively failed to set up the MutationObserver in a way that would make
the IE11 bug appear.

Closes #12061
@shahata
Copy link
Contributor

shahata commented Jun 23, 2015

Spent a whole day nailing down this issue in one of our apps just to find out it is already reported and resolved :)

@gkalpak
Copy link
Member

gkalpak commented Jun 23, 2015

😆

@petebacondarwin
Copy link
Contributor

Got to keep on the bleeding edge @shahata !

netman92 pushed a commit to netman92/angular.js that referenced this issue Aug 8, 2015
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
netman92 pushed a commit to netman92/angular.js that referenced this issue Aug 8, 2015
The fixed test is supposed to test a fix for an IE11 bug/peculiarity that
arises when using a specifically configured MutationObserver on the page
(see angular#11781 for more info).
The configuration contained a typo (`sublist` instead of `subtree`), which
effectively failed to set up the MutationObserver in a way that would make
the IE11 bug appear.

Closes angular#12061
smdvdsn added a commit to smdvdsn/angular.js that referenced this issue Aug 18, 2015
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
smdvdsn added a commit to smdvdsn/angular.js that referenced this issue Aug 18, 2015
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
smdvdsn added a commit to smdvdsn/angular.js that referenced this issue Aug 19, 2015
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
smdvdsn added a commit to smdvdsn/angular.js that referenced this issue Aug 19, 2015
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 i@0.3.2 was unpublished from npm.

Closes angular#11781
Narretz pushed a commit that referenced this issue Sep 1, 2015
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 i@0.3.2 was unpublished from npm.

Closes #11781
Closes #12613
@jshoudy11
Copy link

A similar error to this is coming up again in IE11 but only in CJK and Cyrillic languages. Must be related to how IE11 parses non-latin languages?

@jshoudy11
Copy link

It is splitting the interpolated string after the curly brackets. So it is becoming

{{
interpolatedText}}

@gkalpak
Copy link
Member

gkalpak commented Jun 6, 2016

@jshoudy11, do you have a demo ?

@jshoudy11
Copy link

This is actually a public facing issue on our website right now.

  • Go to https://console.cloud.google.com on IE11
  • Click on the vertical "..." in the upper right hand corner
  • Under "preferences" click "Language and regional formats"
  • Choose one of the CJK languages
  • Let the page reload and observe the "{{cardCtrl.project.name}}" in the top left card of the Dashboard

@Narretz
Copy link
Contributor

Narretz commented Jun 6, 2016

You should open a new issue for that so it's easier to handle. We'll mark it as origin: google too. And a small self contained demo is still valuable if you have one.

@gkalpak
Copy link
Member

gkalpak commented Jun 6, 2016

I couldn't reproduce it. It also doesn't make sense that changing an app setting would affect how the browser parses stuff. It was either a different problem or is related to the user's locale.

If you can reliably reproduce the problem, please open a new issue as @Narretz suggested (providing all relevant info, such as OS/version, browser/version, your locale etc).

@petebacondarwin
Copy link
Contributor

@jshoudy11 are you using ngMessageFormat in the app that is causing problems?

@jshoudy11
Copy link

No. Not using ngMessageFormat. I'm trying to get a self contained demo that repros. I'll open a new issue if I get that.

@jshoudy11
Copy link

Also to get the repro follow the same instructions as before except look for "{{::link.text}} in the bottom right of the reloaded page

gkalpak added a commit to gkalpak/angular.js that referenced this issue Aug 15, 2016
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>
   ```
gkalpak added a commit to gkalpak/angular.js that referenced this issue Aug 15, 2016
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>
   ```
gkalpak added a commit that referenced this issue Aug 25, 2016
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>
   ```
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants