Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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/

like image 774
Neil Sarkar Avatar asked Nov 19 '14 03:11

Neil Sarkar


People also ask

What does $compile do in AngularJS?

Overview. Compiles an HTML string or DOM into a template and produces a template function, which can then be used to link scope and the template together. The compilation is a process of walking the DOM tree and matching DOM elements to directives. Note: This document is an in-depth reference of all directive options.

What is templateUrl in AngularJS?

templateUrl can also be a function which returns the URL of an HTML template to be loaded and used for the directive. AngularJS will call the templateUrl function with two parameters: the element that the directive was called on, and an attr object associated with that element.

Which directive link AngularJS with HTML?

AngularJS Directives The ng-model directive binds the value of HTML controls (input, select, textarea) to application data.

What is directive in AngularJS with example?

Directives are markers on the DOM element which tell AngularJS to attach a specified behavior to that DOM element or even transform the DOM element with its children. Simple AngularJS allows extending HTML with new attributes called Directives.


1 Answers

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.

like image 152
GregL Avatar answered Sep 22 '22 06:09

GregL