41

I'm making a directive that modifies it's inner html. Code so far:

.directive('autotranslate', function($interpolate) {
    return function(scope, element, attr) {
      var html = element.html();
      debugger;
      html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
        return '<span translate="' + text + '"></span>';
      });
      element.html(html);
    }
  })

It works, except that the inner html is not evaluated by angular. I want to trigger a revaluation of element's subtree. Is there a way to do that?

Thanks :)

kornfridge
  • 5,102
  • 6
  • 28
  • 40

3 Answers3

54

You have to $compile your inner html like

.directive('autotranslate', function($interpolate, $compile) {
    return function(scope, element, attr) {
      var html = element.html();
      debugger;
      html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
        return '<span translate="' + text + '"></span>';
      });
      element.html(html);
      $compile(element.contents())(scope); //<---- recompilation 
    }
  })
Tasnim Reza
  • 6,058
  • 3
  • 25
  • 30
  • Here's what I'm using it for: http://stackoverflow.com/questions/21483857/add-another-custom-interpolator-in-angularjs/21489208#21489208 – kornfridge Jan 31 '14 at 20:16
  • IMHO it's not correct to re-compile an already compiled element (see the problems reported in @kornfridge's solution below). – jerico Mar 19 '15 at 10:06
  • @jerico Inside of `link` function must you have to compile again when you are manipulating your `DOM` unless you are using `compile` function. You mentioned @kornfridge's solution, he used `compile` function. According to documentation https://docs.angularjs.org/guide/compiler#the-difference-between-compile-and-link i don't think this is the proper use case to use `compile` function. – Tasnim Reza Mar 22 '15 at 04:52
  • 1
    @Reza: IMO the compile function works but is not the best solution either. The OP's intent could probably best be met by manipulating markup on the server, [through an interceptor](https://docs.angularjs.org/api/ng/service/$http#interceptors) or in the [template cache](https://docs.angularjs.org/api/ng/service/$templateCache) *before* compilation. This makes for a highly performant and stable solution as it completely removes the need for Angular bindings in translation. See how Google's [DoubleClick](https://www.youtube.com/watch?v=ee3Ecw8rl1Y) did it (starts around 42:34 in the video). – jerico Mar 22 '15 at 13:21
  • @Reza: The documentation you cite states that the compile function is to manipulate the structure of the markup while the link function is just to bind it to a scope. As the OP wants to manipulate the markup including the bindings, I'd say that is quite obviously a use case of the compile function (but see my other comment for a better solution in this special case). – jerico Mar 22 '15 at 13:29
  • Added an up-vote here for the last line with $compile... using element.contents() instead of elements.html() was not obvious and got me hung up. Thanks! – Scott Byers Feb 10 '16 at 03:27
19

Here's a more generic method I developed to solve this problem:

angular.module('kcd.directives').directive('kcdRecompile', function($compile, $parse) {
  'use strict';
  return {
    scope: true, // required to be able to clear watchers safely
    compile: function(el) {
      var template = getElementAsHtml(el);
      return function link(scope, $el, attrs) {
        var stopWatching = scope.$parent.$watch(attrs.kcdRecompile, function(_new, _old) {
          var useBoolean = attrs.hasOwnProperty('useBoolean');
          if ((useBoolean && (!_new || _new === 'false')) || (!useBoolean && (!_new || _new === _old))) {
            return;
          }
          // reset kcdRecompile to false if we're using a boolean
          if (useBoolean) {
            $parse(attrs.kcdRecompile).assign(scope.$parent, false);
          }

          // recompile
          var newEl = $compile(template)(scope.$parent);
          $el.replaceWith(newEl);

          // Destroy old scope, reassign new scope.
          stopWatching();
          scope.$destroy();
        });
      };
    }
  };

  function getElementAsHtml(el) {
    return angular.element('<a></a>').append(el.clone()).html();
  }
});

You use it like so:

HTML

<div kcd-recompile="recompile.things" use-boolean>
  <div ng-repeat="thing in ::things">
    <img ng-src="{{::thing.getImage()}}">
    <span>{{::thing.name}}</span>
  </div>
</div>

JavaScript

$scope.recompile = { things: false };
$scope.$on('things.changed', function() { // or some other notification mechanism that you need to recompile...
  $scope.recompile.things = true;
});

Edit

If you're looking at this, I would seriously recommend looking at the website's version as that is likely to be more up to date.

kentcdodds
  • 27,113
  • 32
  • 108
  • 187
  • @Adam Barthelson, haha, thanks :-) I actually updated this on my site recently. So I would recommend looking at the site's version. – kentcdodds Aug 20 '14 at 18:11
  • Can this directive be used to trigger a recompliation in angular 1.2? Do I have to use one-time binding expressions from angular 1.3? – swenedo Aug 27 '14 at 12:04
  • Can someone explain me: `return angular.element('').append(el.clone()).html();` – Krzysztof Kaczor Sep 19 '14 at 10:54
  • Yes, `.html()` returns the contents of the element. Because we also want the root node of `el`, we first insert it into a fake element and then get the contents of that. – kentcdodds Sep 19 '14 at 11:32
  • Access to the $parent scope from within a directive breaks encapsulation. If you want to do that then you should require a parent directive (see the require-attribute of the Angular directive configuration object) to establish such a dependency in a supported way. – jerico Mar 19 '15 at 10:10
  • @kentcdodds - sorry for the asking here ... but can you see anything wrong in the way I'm using the directive? http://codepen.io/anon/pen/EjWGKd?editors=101 – sirrocco Jun 04 '15 at 15:33
  • You're specifying `xcd-use-boolean`. It should be: `kcd-use-boolean` – kentcdodds Jun 04 '15 at 16:33
  • Thanks, but that's not all - I created a question to stop polluting this: http://stackoverflow.com/q/30650619/5246 – sirrocco Jun 04 '15 at 17:42
10

This turned out to work even better than @Reza's solution

.directive('autotranslate', function() {
  return {
    compile: function(element, attrs) {
      var html = element.html();
      html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
        return '<span translate="' + text + '"></span>';
      });
      element.html(html);
    }
  };
})

Reza's code work when scope is the scope for all of it child elements. However, if there's an ng-controller or something in one of the childnodes of this directive, the scope variables aren't found. However, with this solution ^, it just works!

kornfridge
  • 5,102
  • 6
  • 28
  • 40
  • @kornifridge can you make it clear `Reza's code work when scope is the scope for all of it child elements` ? and how it is better ? according to documentation https://docs.angularjs.org/guide/compiler#the-difference-between-compile-and-link the best practice of use `compile` function is `Any operation which can be shared among the instance of directives should be moved to the compile function for performance reasons.` – Tasnim Reza Mar 22 '15 at 04:59