6

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/

2 Answers 2

24

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.

Sign up to request clarification or add additional context in comments.

7 Comments

yep, that makes total sense. For some reason I assumed angular would compile detached dom nodes although now that I say that out loud we're just talking about a string, not DOM nodes. thanks for the succinct description of $digest vs $apply also :)
@NeilSarkar It turns out you were right about it being able to compile detached DOM nodes. I updated my answer with an explanation about why your approach just missed the mark.
I would recommend EXTREME caution when calling $scope.$digest() in a production app. The correct way to make a digest occur is to call $apply.
That's an interesting stance, Enzey. I'd love to engage with discussion about this in chat if you're up for it.
@GregL so, turns out that your solution works for the template case, but not templateurl. see this updated fiddle. I'm digging through the angular source now to see how templateUrl actually works, but if you can think of a solution for this updated problem, I'd appreciate it: jsfiddle.net/trz80n9y/3
|
4

Append the element to the target first, then compile it.

html = angular.element(html);
target = angular.element(target);
target.append(html);
html = $compile(html)($scope)

http://jsfiddle.net/f3dkp291/16/

3 Comments

This is basically the TL;DR version of my answer. :-)
sadly yes, yes it is.
Missing a digest, no?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.