96

Plunker Link

I have a element which I would like to bind html to it.

<div ng-bind-html="details" upper></div>

That works. Now, along with it I also have a directive which is bound to the bound html:

$scope.details = 'Success! <a href="#/details/12" upper>details</a>'

But the directive upper with the div and anchor do not evaluate. How do I make it work?

GG.
  • 21,083
  • 14
  • 84
  • 130
Amitava
  • 5,013
  • 9
  • 37
  • 50
  • 3
    Look at my answer here http://stackoverflow.com/questions/17343696/adding-an-ng-click-event-inside-a-filter/17344875#17344875 – Chandermani Jul 02 '13 at 06:17
  • @Chandermani not exactly using directive inside ng-bind-html-unsafe but using filter. But it will do, I just created a filter and passed to the directive. Thanks! – Amitava Jul 03 '13 at 04:27
  • @SamSerious are you able to show how you did what you did for the filters? – CMCDragonkai Sep 26 '13 at 21:56
  • the above solutions don't handle multiple changes of the value a better solution http://stackoverflow.com/a/25516311/3343425 – fghibellini Aug 26 '14 at 22:50

6 Answers6

187

I was also facing this problem and after hours searching the internet I read @Chandermani's comment, which proved to be the solution. You need to call a 'compile' directive with this pattern:

HTML:

<div compile="details"></div>

JS:

.directive('compile', ['$compile', function ($compile) {
    return function(scope, element, attrs) {
        scope.$watch(
            function(scope) {
                // watch the 'compile' expression for changes
                return scope.$eval(attrs.compile);
            },
            function(value) {
                // when the 'compile' expression changes
                // assign it into the current DOM
                element.html(value);

                // compile the new DOM and link it to the current
                // scope.
                // NOTE: we only compile .childNodes so that
                // we don't get into infinite loop compiling ourselves
                $compile(element.contents())(scope);
            }
        );
    };
}])

You can see a working fiddle of it here

Slava Fomin II
  • 26,865
  • 29
  • 124
  • 202
vkammerer
  • 2,047
  • 1
  • 14
  • 9
  • 1
    In line #2, ie. `function(scope, element, attrs)`, where did you get from those three arguments, _scope_, _element_ and _attrs_? – spaffy Feb 06 '15 at 15:16
  • 1
    @spaffy - they're part of Angular framework's signature for the `link` property. They'll be passed automatically each time when `link` is called by the Angular framework. They'll always be available. – Ben Apr 28 '15 at 17:35
  • 1
    Well done. You saved me those same hours of searching. I'm pulling content from SharePoint view REST API, which itself contains Angular markup such as ng-repeat. Your directive made it all work. Thanks! – Phil Nicholas Nov 05 '15 at 00:28
  • Thanks for your directive it fixed the problems I was having. Now the angular code gets compiled but too many times. A ng-repeat with 3 object turns into the same values just 3x each. Whats going wrong here? – Jason Dec 05 '16 at 11:47
  • 2
    If you have been using `$sce.trustAsHtml` from another function to create the HTML that will be "compiled" with this directive, you should remove it. Thanks to @apoplexy – Burak Tokak Jan 01 '17 at 10:46
36

Thanks for the great answer vkammerer. One optimization I would recommend is un-watching after the compilation runs once. The $eval within the watch expression could have performance implications.

    angular.module('vkApp')
  .directive('compile', ['$compile', function ($compile) {
      return function(scope, element, attrs) {
          var ensureCompileRunsOnce = scope.$watch(
            function(scope) {
               // watch the 'compile' expression for changes
              return scope.$eval(attrs.compile);
            },
            function(value) {
              // when the 'compile' expression changes
              // assign it into the current DOM
              element.html(value);

              // compile the new DOM and link it to the current
              // scope.
              // NOTE: we only compile .childNodes so that
              // we don't get into infinite loop compiling ourselves
              $compile(element.contents())(scope);

              // Use un-watch feature to ensure compilation happens only once.
              ensureCompileRunsOnce();
            }
        );
    };
}]);

Here's a forked and updated fiddle.

user3075469
  • 369
  • 3
  • 3
28

Add this directive angular-bind-html-compile

.directive('bindHtmlCompile', ['$compile', function ($compile) {
  return {
    restrict: 'A',
    link: function (scope, element, attrs) {
      scope.$watch(function () {
        return scope.$eval(attrs.bindHtmlCompile);
      }, function (value) {
        // Incase value is a TrustedValueHolderType, sometimes it
        // needs to be explicitly called into a string in order to
        // get the HTML string.
        element.html(value && value.toString());
        // If scope is provided use it, otherwise use parent scope
        var compileScope = scope;
        if (attrs.bindHtmlScope) {
          compileScope = scope.$eval(attrs.bindHtmlScope);
        }
        $compile(element.contents())(compileScope);
      });
    }
  };
}]);

Use it like this :

<div bind-html-compile="data.content"></div>

Really easy :)

Joël
  • 1,563
  • 1
  • 18
  • 31
  • 1
    Be careful, if you pass something like this: "$scope.loadContent = function() { return $sce.trustAsHtml(require('html/main-content.html')); };" to it you can get infinite digest loop. Without the trustAsHtml it works. – Lakatos Gyula Dec 16 '15 at 21:52
  • I tried this option. Works like a charm! I also use $sce.trustAsHtml before passing it to the directive. Thanks! – Manoj Lasantha May 18 '22 at 01:43
13

Unfortunately I don't have enough reputation to comment.

I could not get this to work for ages. I modified my ng-bind-html code to use this custom directive, but I failed to remove the $scope.html = $sce.trustAsHtml($scope.html) that was required for ng-bind-html to work. As soon as I removed this, the compile function started to work.

apoplexy
  • 323
  • 2
  • 9
6

For anyone dealing with content that has already been run through $sce.trustAsHtml here is what I had to do differently

function(scope, element, attrs) {
    var ensureCompileRunsOnce = scope.$watch(function(scope) {
            return $sce.parseAsHtml(attrs.compile)(scope);
        },
        function(value) {
            // when the parsed expression changes assign it into the current DOM
            element.html(value);

            // compile the new DOM and link it to the current scope.
            $compile(element.contents())(scope);

            // Use un-watch feature to ensure compilation happens only once.
            ensureCompileRunsOnce();
        });
}

This is only the link portion of the directive as I'm using a different layout. You will need to inject the $sce service as well as $compile.

MStrutt
  • 819
  • 6
  • 7
-2

Best solution what I've found! I copied it and it work's exactly as I needed. Thanks, thanks, thanks ...

in directive link function I have

app.directive('element',function($compile){
  .
  .
     var addXml = function(){
     var el = $compile('<xml-definitions definitions="definitions" />')($scope);
     $scope.renderingElement = el.html();
     }
  .
  .

and in directive template:

<span compile="renderingElement"></span>
BenMorel
  • 34,448
  • 50
  • 182
  • 322
yustme
  • 128
  • 1
  • 8