Escaping & > characters in ng-bind in AngularJs
Asked Answered
T

6

18

I have a use case, where we can have '&' and '>' characters in a string. eg. Johnson & Johnson, value > 3. So while the response from server is encoded, hence the value becomes 'value > 3'.

ng-bind doesn't support the following:

value > 3 will be rendered for ngBind, whereas the browser renders the same content as value > 3.

http://jsfiddle.net/HKahG/2/

Ng:bind <div ng-bind="model"></div> 
Ng:bind-html <div ng-bind-html="model"></div>
<div> From Div: value &gt; </div>

Why is this default browser behavior not present in ng-bind?. I don't want to use ng-bind-html (has issues with value < and it is not a html) or ng-bind-unsafe-html.

My application has dynamic key-value fields which will be displayed in different parts of the application. So it will require additional overhead to use a separate directive or decorator to display all string fields than to use ngBind.

Questions:

1) Is there any other way of doing the same without using an additional directive, or is this the right way of handling encoded data?

2) Can I override the behavior of ng-bind or decorate it by default?

Transfix answered 9/10, 2013 at 6:21 Comment(0)
A
14

EDIT: please, go straight to the bottom of the answer to get the best version; the answer is at chronological order; I got the optimal code after a few iterations, at the end. Thank you.

  • Can I override the behaviour of ng-bind or decorate it by default ?

Yes. I've done a very simple implementation which makes ng-bind to behave as you want. Well... I'm not sure if this is exactly what you want, but at least it does what I've understood you want.

Working fiddle: http://jsfiddle.net/93QQM/

And here is the code:

module.directive('ngBind', function() {
    return {
        compile: function(tElement, tAttrs) {
            tAttrs.ngBind = 'myBind(' + tAttrs.ngBind + ')';
            return { 
                pre: function(scope) {
                    scope.myBind = function(text) {
                        return angular.element('<div>' + text + '</div>').text();
                    }
                }
            };
        }
    }
});

This is not exactly an "additional directive" - this is the way to "override the behaviour of ng-bind". It does not add a new directive, it just extends behaviour of existent ngBind directive.

At the compile function, we modify the value of the ng-bind attribute, wrapping it into a function call. With this, we have access to the original model value, and the opportunity to return it modified.

We make the function available through the scope in the pre-linking phase, because if we do this in the post-linking phase, the function will be available only after the original ngBind directive has retrieved the value from the attribute (which will be an empty string, because the function wil not be found).

The myBind function is simple and smart: it creates an element, and the text is used - unchanged - as the element body, only to be immediately retrieved through the text function - which will return the contents just as "the browser renders" it.

This way, you can use ngBind as usual, like <div ng-bind="model.content" />, but have this modified behaviour.


Improved version

Instead of attaching the myBind function to every scope where ngBind is applied, at every pre-linking phase, we can attach it only once to the $rootScope, making it immediately available for all scopes.

New working fiddle: http://jsfiddle.net/EUqP9/

New code:

module.directive('ngBind', ['$rootScope', function($rootScope) {
    $rootScope.myBind = function(text) {
        return angular.element('<div>' + text + '</div>').text();
    };
    return {
        compile: function(tElement, tAttrs) {
            tAttrs.ngBind = 'myBind(' + tAttrs.ngBind + ')';
        }
    };
}]);

Much cleaner than the previous version! Of course, you can change myBind function name to any other name you want. The "cost" of the feature is this: have this simple function added to the root scope - it is up to you to decide if it worths the price.


Yet another version

Influenced by Chemiv's answer... why not remove the function from any scope and make it a filter instead? It also works.

Yet another new working fiddle: http://jsfiddle.net/hQJaZ/

And the new code:

module.filter('decode', function() {
    return function(text) {
        return angular.element('<div>' + text + '</div>').text();
    };
}).directive('ngBind', function() {
    return {
        compile: function(tElement, tAttrs) {
            tAttrs.ngBind += '|decode';
        }
    };
});

Now you have three options to choose from the menu.

Astrodynamics answered 12/10, 2013 at 2:31 Comment(2)
Why don't put the best (the last) version to the top? As it is done in many other answers.Magruder
@DmitryGonchar - good suggestion; I do not have availability to properly edit the answer now; I've just quickly added a comment at the top, warning the best version is at the bottom.Astrodynamics
G
12

This is HTML:

&gt;

It may not have HTML tags, but it's still HTML. If you want to use ng-bind, your server needs to return unencoded text. Ie, > instead of &gt;.

Use ng-bind-html or modify your server to return plain text without html encoding it first.

Edit: Quick demo that illustrates the use of &gt; and > in JavaScript:

div1.innerHTML = "&gt;";  // write HTML
div2.textContent = ">";   // write plain text
console.log(div1.innerHTML === div2.innerHTML);
console.log(div1.textContent === div2.textContent);

http://jsfiddle.net/XhEcV/

Ghetto answered 9/10, 2013 at 9:6 Comment(7)
&gt; is a reserved characters for displaying '>', helps not to confuse with a tag. Browser renders that correctly, at the end we want to display content in the page part of html content right. I cant return unencoded text from server because of XSS vulnerabilities that can happen (i know angular prevents that). Thanks for the answer.Transfix
So, your server returns HTML. Use ng-bind-html.Ghetto
Server returns JSON encoded data and it is used to display in the content. But anyways, ng-bind-html throws Parser error: with text displaying like 'val < 3'. Because it expects a html if it sees '<'. Why '<' not &gt; ? the user still sees as '<' content in text box even though it is encoded only for display (stored as '<' in the backend).Transfix
Are you saying that your server is encoding >, but not <? If so, then you have a mix of html and plain text that simply cannot be reliably parsed.Ghetto
No problem in encoding in sever side. While displaying it to user, we will still show it as 'Johnson & Johnson' in text box right. Similarly, if you type a character '<' and display it in 'ng-bind-html' without encoding (it is stored as the user has entered), it will throw error. Look at this fiddle. jsfiddle.net/HBk9p/1 - look at the console.Transfix
Ok, I'm with you now. You are getting HTML from the server, but you are also binding to form controls, which, of course, give you plain text. You want to be able to bind as HTML when it comes from the server, but otherwise bind as plain text when the values come from the form. Sadly, I can't be much help... I just don't know.Ghetto
Your example of typing > in a input and then seeing it render perfectly fine in the ng-bind isn't a proper example... when you type the character it is already decoded and rendered as plane text hence why it can be seen in ng-bind... if you want the elements to be seen in ng-bind you either need to have your backend return html elements as decoded plain text or create a directive that will decode it to plane text first before turning itCaloric
E
6

ng-bind uses .text() method to replace text and while your code contains &gt; which is HTML markup it is not correctly rendered by ng-bind. You should use ng-bind-html in this place as you are actually entering HTML content. Otherwise you can replace > by regex to '>'.

ex :- model = model.replace(/&gt;/g, '>');

But in this case you have to replace all HTML markups which is not needed since ng-bind-html is already working fine in your case.

Eakins answered 16/10, 2013 at 14:20 Comment(0)
H
5

Yes , let's "decorate" it with a filter:

.filter("decode",function(){
    return function(str){         
      var el = document.createElement("div");
      el.innerHTML = str;
      str =   el.textContent || el.innerText;
      return str;        
    }
});

And use it like: <div ng-bind="model|decode"></div>

Working example: http://jsfiddle.net/HKahG/5/

Inspired by this answer: https://mcmap.net/q/37656/-convert-special-characters-to-html-in-javascript

Heldentenor answered 9/10, 2013 at 7:56 Comment(5)
Shouldn't you check textContent first, being it's the official property?Replace
I have updated the question, sorry that I missed certain things. I know to do this, but how can this done by default for ng-bind.Transfix
@Transfix i'm not so strong in english , but seems to me that it is the default behaviour. The ngBind attribute tells Angular to replace the text content of the specified HTML element with the value of a given expression (from here: docs.angularjs.org/api/ng.directive:ngBind). So "decorating" it is the conventional practice..Heldentenor
@Replace edited ( but the question is not about it ;) )Heldentenor
@Cherniv - what I looking at is - ngBind or {{}} by default should work with the above filter (which you have written). My usage will only look like '<div ng-bind="model.content" /> (that filter should be implicit).Transfix
C
1

I remember a directive named ngBindHtmlUnsafe available for such use cases.

http://code.angularjs.org/1.0.8/docs/api/ng.directive:ngBindHtmlUnsafe

Please refer to this. Not sure if this is available in later unstable releases. This is link to the latest stable release available.

Cattleman answered 14/10, 2013 at 11:10 Comment(0)
S
0

Why not just use $sce.trustAsHtml?

Scuttle answered 19/9, 2015 at 5:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.