AngularJS: Using $compile on html that contains directives with templateurl

Neil Sarkar picture Neil Sarkar · Nov 19, 2014 · Viewed 17.8k times · Source

I have a legacy application that has some content inserted into the DOM via jQuery. I would like the legacy parts of the codebase to be responsible for compiling the html that it inserts into the DOM.

I can get it to compile the initial html using $compile, but any DOM elements added by a directive's template or templateUrl are not compiled, unless I call $scope.$apply() from within the directive itself.

What am I doing wrong here?

Link to fiddle: http://jsfiddle.net/f3dkp291/15/

index.html

<div ng-app="app">
    <debug source='html'></debug>
    <div id="target"></div>
</div>

application.js

angular.module('app', []).directive('debug', function() {
    return {
        restrict: 'E',
        template: "scope {{$id}} loaded from {{source}}",
        link: function($scope, el, attrs) {
          $scope.source = attrs.source

          if( attrs.autoApply ) {
              // this works
              $scope.$apply()
          }
        },
        scope: true
    }
})

// mimic an xhr request
setTimeout(function() {
    var html = "<div><debug source='xhr (auto-applied)' auto-apply='1'></debug><br /><debug source='xhr'></debug></div>",
        target = document.getElementById('target'),
        $injector = angular.injector(['ng','app']),
        $compile = $injector.get('$compile'),
        $rootScope = $injector.get('$rootScope'),
        $scope = angular.element(target).scope();

    target.innerHTML = $compile(html)($scope)[0].outerHTML

    // these do nothing, and I want to compile the directive's template from here.
    $scope.$apply()
    $scope.$root.$apply()
    angular.injector(['ng','app']).get('$rootScope').$apply()
}, 0)

output

scope 003 loaded from html
scope 005 loaded from xhr (auto-applied)
scope {{$id}} loaded from {{source}}

Update: Solution works for directives with a template property, but not templateUrl

So, I should have been compiling dom nodes, not an HTML string. However, this updated fiddle shows the same failing behavior if the directive contains a templateUrl:

http://jsfiddle.net/trz80n9y/3/

Answer

GregL picture GregL · Nov 19, 2014

As you probably realised, you need to call $scope.$apply() for it to update the {{bindings}} from the scope values.

But the reason you couldn't do it inside your async function was that you were compiling the HTML against the existing scope for #target, but then trying to append just the HTML. That won't work, because you need to have the compiled node in the DOM, either by appending the entire compiled node using jQuery's .append() or similar, or by setting the DOM innerHTML first, then compiling the node that is in the DOM. After that, you can call $apply on that scope and because the directive is compiled and in the DOM, it will be updated correctly.

In other words, change your async code as follows.

Instead of:

target.innerHTML = $compile(html)($scope)[0].outerHTML
$scope.$apply()

Change it to:

target.innerHTML = html;
$compile(target)($scope);
$scope.$digest();

Note that I did a $digest() instead of $apply(). This is because $apply() does a digest of every single scope, starting from the $rootScope. You only need to digest that one scope you linked against, so it is sufficient (and faster, for any reasonably sized app with lots of scopes) to just digest that one.

Forked fiddle

Update: Angular can compile strings and detached DOM nodes

I just checked, and the OP was actually correct in assuming that Angular can compile strings of HTML or detached DOM nodes just fine. But what you do need to do is make sure you actually append the compiled node to the DOM, not just the HTML. This is because Angular stores things like the scope and the binding information as jQuery/jQueryLite data on the DOM node*. Thus you need to append the whole node, with that extra information, so that the $digest() will work.

So an alternative way of having this work is to change the same portion of the OP's code as above to:

target.appendChild($compile(html)($scope)[0]);
$scope.$digest()

* Technically, it is stored in the internal jQuery data cache, with the cache key being stored on the DOM node itself.