How do I make angular.js reevaluate / recompile inner html?

44,670

Solution 1

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 
    }
  })

Solution 2

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.

Solution 3

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!

Share:
44,670
kornfridge
Author by

kornfridge

Updated on August 22, 2020

Comments

  • kornfridge
    kornfridge almost 4 years

    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
    kornfridge over 10 years
    Here's what I'm using it for: stackoverflow.com/questions/21483857/…
  • kentcdodds
    kentcdodds almost 10 years
    @Adam Barthelson, haha, thanks :-) I actually updated this on my site recently. So I would recommend looking at the site's version.
  • swenedo
    swenedo almost 10 years
    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?
  • Krzysztof Kaczor
    Krzysztof Kaczor almost 10 years
    Can someone explain me: return angular.element('<a></a>').append(el.clone()).html();
  • kentcdodds
    kentcdodds almost 10 years
    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.
  • jerico
    jerico over 9 years
    IMHO it's not correct to re-compile an already compiled element (see the problems reported in @kornfridge's solution below).
  • jerico
    jerico over 9 years
    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.
  • Tasnim Reza
    Tasnim Reza over 9 years
    @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 docs.angularjs.org/guide/… i don't think this is the proper use case to use compile function.
  • Tasnim Reza
    Tasnim Reza over 9 years
    @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 docs.angularjs.org/guide/… 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.
  • jerico
    jerico over 9 years
    @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 or in the template cache 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 did it (starts around 42:34 in the video).
  • jerico
    jerico over 9 years
    @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).
  • sirrocco
    sirrocco about 9 years
    @kentcdodds - sorry for the asking here ... but can you see anything wrong in the way I'm using the directive? codepen.io/anon/pen/EjWGKd?editors=101
  • kentcdodds
    kentcdodds about 9 years
    You're specifying xcd-use-boolean. It should be: kcd-use-boolean
  • sirrocco
    sirrocco about 9 years
    Thanks, but that's not all - I created a question to stop polluting this: stackoverflow.com/q/30650619/5246
  • Scott Byers
    Scott Byers over 8 years
    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!