Compiling dynamic HTML strings from database

158,878

Solution 1

ng-bind-html-unsafe only renders the content as HTML. It doesn't bind Angular scope to the resulted DOM. You have to use $compile service for that purpose. I created this plunker to demonstrate how to use $compile to create a directive rendering dynamic HTML entered by users and binding to the controller's scope. The source is posted below.

demo.html

<!DOCTYPE html>
<html ng-app="app">

  <head>
    <script data-require="[email protected]" data-semver="1.0.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.js"></script>
    <script src="script.js"></script>
  </head>

  <body>
    <h1>Compile dynamic HTML</h1>
    <div ng-controller="MyController">
      <textarea ng-model="html"></textarea>
      <div dynamic="html"></div>
    </div>
  </body>

</html>

script.js

var app = angular.module('app', []);

app.directive('dynamic', function ($compile) {
  return {
    restrict: 'A',
    replace: true,
    link: function (scope, ele, attrs) {
      scope.$watch(attrs.dynamic, function(html) {
        ele.html(html);
        $compile(ele.contents())(scope);
      });
    }
  };
});

function MyController($scope) {
  $scope.click = function(arg) {
    alert('Clicked ' + arg);
  }
  $scope.html = '<a ng-click="click(1)" href="#">Click me</a>';
}

Solution 2

In angular 1.2.10 the line scope.$watch(attrs.dynamic, function(html) { was returning an invalid character error because it was trying to watch the value of attrs.dynamic which was html text.

I fixed that by fetching the attribute from the scope property

 scope: { dynamic: '=dynamic'}, 

My example

angular.module('app')
  .directive('dynamic', function ($compile) {
    return {
      restrict: 'A',
      replace: true,
      scope: { dynamic: '=dynamic'},
      link: function postLink(scope, element, attrs) {
        scope.$watch( 'dynamic' , function(html){
          element.html(html);
          $compile(element.contents())(scope);
        });
      }
    };
  });

Solution 3

Found in a google discussion group. Works for me.

var $injector = angular.injector(['ng', 'myApp']);
$injector.invoke(function($rootScope, $compile) {
  $compile(element)($rootScope);
});

Solution 4

You can use

ng-bind-html https://docs.angularjs.org/api/ng/service/$sce

directive to bind html dynamically. However you have to get the data via $sce service.

Please see the live demo at http://plnkr.co/edit/k4s3Bx

var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope,$sce) {
    $scope.getHtml=function(){
   return $sce.trustAsHtml("<b>Hi Rupesh hi <u>dfdfdfdf</u>!</b>sdafsdfsdf<button>dfdfasdf</button>");
   }
});

  <body ng-controller="MainCtrl">
<span ng-bind-html="getHtml()"></span>
  </body>

Solution 5

Try this below code for binding html through attr

.directive('dynamic', function ($compile) {
    return {
      restrict: 'A',
      replace: true,
      scope: { dynamic: '=dynamic'},
      link: function postLink(scope, element, attrs) {
        scope.$watch( 'attrs.dynamic' , function(html){
          element.html(scope.dynamic);
          $compile(element.contents())(scope);
        });
      }
    };
  });

Try this element.html(scope.dynamic); than element.html(attr.dynamic);

Share:
158,878
giraffe_sense
Author by

giraffe_sense

Updated on January 15, 2020

Comments

  • giraffe_sense
    giraffe_sense over 4 years

    The Situation

    Nested within our Angular app is a directive called Page, backed by a controller, which contains a div with an ng-bind-html-unsafe attribute. This is assigned to a $scope var called 'pageContent'. This var gets assigned dynamically generated HTML from a database. When the user flips to the next page, a called to the DB is made, and the pageContent var is set to this new HTML, which gets rendered onscreen through ng-bind-html-unsafe. Here's the code:

    Page directive

    angular.module('myApp.directives')
        .directive('myPage', function ($compile) {
    
            return {
                templateUrl: 'page.html',
                restrict: 'E',
                compile: function compile(element, attrs, transclude) {
                    // does nothing currently
                    return {
                        pre: function preLink(scope, element, attrs, controller) {
                            // does nothing currently
                        },
                        post: function postLink(scope, element, attrs, controller) {
                            // does nothing currently
                        }
                    }
                }
            };
        });
    

    Page directive's template ("page.html" from the templateUrl property above)

    <div ng-controller="PageCtrl" >
       ...
       <!-- dynamic page content written into the div below -->
       <div ng-bind-html-unsafe="pageContent" >
       ...
    </div>
    

    Page controller

    angular.module('myApp')
      .controller('PageCtrl', function ($scope) {
    
            $scope.pageContent = '';
    
            $scope.$on( "receivedPageContent", function(event, args) {
                console.log( 'new page content received after DB call' );
                $scope.pageContent = args.htmlStrFromDB;
            });
    
    });
    

    That works. We see the page's HTML from the DB rendered nicely in the browser. When the user flips to the next page, we see the next page's content, and so on. So far so good.

    The Problem

    The problem here is that we want to have interactive content inside of a page's content. For instance, the HTML may contain a thumbnail image where, when the user clicks on it, Angular should do something awesome, such as displaying a pop-up modal window. I've placed Angular method calls (ng-click) in the HTML strings in our database, but of course Angular isn't going to recognize either method calls or directives unless it somehow parses the HTML string, recognizes them and compiles them.

    In our DB

    Content for Page 1:

    <p>Here's a cool pic of a lion. <img src="lion.png" ng-click="doSomethingAwesone('lion', 'showImage')" > Click on him to see a large image.</p>
    

    Content for Page 2:

    <p>Here's a snake. <img src="snake.png" ng-click="doSomethingAwesone('snake', 'playSound')" >Click to make him hiss.</p>
    

    Back in the Page controller, we then add the corresponding $scope function:

    Page controller

    $scope.doSomethingAwesome = function( id, action ) {
        console.log( "Going to do " + action + " with "+ id );
    }
    

    I can't figure out how to call that 'doSomethingAwesome' method from within the HTML string from the DB. I realize Angular has to parse the HTML string somehow, but how? I've read vague mumblings about the $compile service, and copied and pasted some examples, but nothing works. Also, most examples show dynamic content only getting set during the linking phase of the directive. We would want Page to stay alive throughout the life of the app. It constantly receives, compiles and displays new content as the user flips through pages.

    In an abstract sense, I guess you could say we are trying to dynamically nest chunks of Angular within an Angular app, and need to be able to swap them in and out.

    I've read various bits of Angular documentation multiple times, as well as all sorts of blog posts, and JS Fiddled with people's code. I don't know whether I'm completely misunderstanding Angular, or just missing something simple, or maybe I'm slow. In any case, I could use some advice.

  • giraffe_sense
    giraffe_sense almost 11 years
    Thanks so much, Buu! Creating the attribute directive and adding the scope watch function were the two things I was missing. Now that this is working, guess I'll read up again on directives and $compile, to better understand what's going on under the hood.
  • Craig Morgan
    Craig Morgan over 10 years
    Me too!The Angular team could really do with improving the docs on this.
  • DzeryCZ
    DzeryCZ over 10 years
    Hello, If I use element.html it return me TypeError: Cannot call method 'insertBefore' of null. So after some googling about that I find that I must use element.append But If I use that directive on multiple places - it generate multiplicate HTML. So 2 directives generate 4 same HTML code. Thanks for your answer.
  • Alexandros Spyropoulos
    Alexandros Spyropoulos over 10 years
    I wouldn't use append in your place, I will have a look on that tonight and I'll get back to you. To be honest, I used this directive in quite a few places in a page without any issue. I'll try to reproduce the problem and I'll get back to you.
  • Buu
    Buu over 10 years
    @AlexandrosSpyropoulos I just test and see that my code runs okay even with 1.2.12. I think you probably missed the declaration <div dynamic="html"> in the HTML? (With that declaration, $watch watches the 'html' property in scope, not the actual HTML as you mentioned, so there should be no invalid char error.) If not, send me the plunkr that shows it doesn't work, I'll see what's wrong.
  • Alexandros Spyropoulos
    Alexandros Spyropoulos about 10 years
    You're probably right. I've been expecting back then, that html is actually a variable that contains html :P. It's good idea though to set a scope on your Directives. umur.io/…
  • Mital Pritmani
    Mital Pritmani about 10 years
    $compile(ele.contents())(scope); - this line solved my issue of not compiling angular components which are added dynamically. Thanks.
  • Mital Pritmani
    Mital Pritmani about 10 years
    $compile(ele.contents())(scope); - this line solved my issue of not compiling angular components which are added dynamically. Thanks.
  • anam
    anam about 10 years
    @BuuNguyen inside teplateURL suppose if u include some dynamic htmnl page using ng-bind-html , then using compile doesnt work gives error from some unsafe content other side using trustAsHTml only remove unsafe error doesnt compile , any suggestions?
  • Buu
    Buu about 10 years
    @simmisimmi it's how ng-bind-html works. Either include $sanitize or use trustAsHtml correctly. See this docs.angularjs.org/api/ng/directive/ngBindHtml
  • AnxiousdeV
    AnxiousdeV about 10 years
    How would you dynamically add "<div dynamic="html"></div>" In my case I need to add table rows that contain ng-click directives so the dynamic directive itself needs to be injected
  • tonejac
    tonejac about 9 years
    So if there are ng-click attributes in the child elements of the container with the dynamic='html' attribute will those also be compiled or do you have to do a separate directive for each element that needs compiling?
  • landed
    landed about 9 years
    I like this example but it doesnt get mine working. I have a switch statement that happens due to user choice so its dynamic. Depending on that I want to insert html containing directive. The directive works if I place it in the natural bootstrap phase. However I have this that is simply not firing --- case 'info': $scope.htmlString = $sce.trustAsHtml('<div dynamic="htmlString">dddzzz</div>'); break; --- when I want to do something like --- $compile($sce.trustAsHtml('<div dynamic="htmlString">dddzzz</div>')); Any ideas on workarounds etc...
  • Suraj Dalvi
    Suraj Dalvi over 8 years
    I use this directive but i am getting following error: Error: [$parse:lexerr] errors.angularjs.org/1.4.7/$parse/… Please help me into this?
  • dzagorovsky
    dzagorovsky almost 8 years
  • Murali Krishna
    Murali Krishna over 7 years
    This is working fine when I declare the HTML(with custom directive) on page load but custom directives are not loading as expected when I get the HTML string from the DB on click, I tried $timeout in the above directive but no use, can anyone suggest an idea?
  • jaggedsoft
    jaggedsoft over 7 years
    Thanks! This helped me. However, you need to include ngSanitize and angular-sanitize.js: var myApp = angular.module('myApp', ['ngSanitize']);
  • changtung
    changtung about 7 years
    that worked for me too during binding bootstrap icon to material md-list span element