I've got a directive that's working like this:
http://jsfiddle.net/smithkl42/cwrgLd0L/23/
App.directive('prettify', ['$compile', function ($compile) {
var templateFn;
return {
restrict: 'E',
scope: {
target: '='
},
link: function (scope, element, attrs) {
if (!templateFn) {
var template = element.html();
templateFn = $compile(template);
}
scope.$watch('target', function (newVal, oldVal) {
var compiled = templateFn(scope);
element.html('');
element.append(compiled);
var html = element.html();
var prettified = prettyPrintOne(html);
element.html(prettified);
}, true);
}
};
}]);
The problem is that when I compile the template, it always compiles with the old value of the target
property. So it starts off showing this, i.e., it's acting like there's nothing to replace:
Then if I add a character to the property, it shows this, i.e., the previous value of the scope.organization.message
property:
Debugging shows that the values in the target
property of the scope directive are correct at the time of the compile.
What am I doing wrong? Is there something about the template function returned by $compile
that looks at the old scope values? Or...?
(See also this question, which led to this one: Using $compile in a directive triggers AngularJS infinite digest error.)
First off, your templateFn
variable is scoped at the factory level, but it is populated at the instance level. This means that the first time you use the directive, it will populate using that element's directive, and every usage after that will also use that same template, even if it actually has a different template.
The cause of your seemingly delayed binding issue has to do with the digest cycle and how Angular manages changes to the DOM. When a scope change is being processed, the scope watchers are all processed before any changes are made to the DOM. This way, all DOM changes are consolidated into a single batch (for that cycle at least) so you aren't make multiple updates at once, potentially causing multiple reflows. So, when you're calling element.html()
, you're doing so at a point where the DOM hasn't been updated to reflect the changed values on the scope.
In this particular case, you're also doing a bunch of extra work - calling templateFn
will give you a jQuery (or jQLite) object with the content you need - there's no need to add it to the DOM, and then take it back out, you can just call html()
directly against it.
That logic could all be consolidated (and work correctly) like so:
setTimeout(function () {
var compiled = templateFn(scope).html();
var prettified = prettyPrintOne(compiled);
element.html(prettified);
}, 0);
Wrapping everything in setTimeout
forces the logic to be evaluated after the digest cycle is complete.
However, generally speaking, that implementation of the directive is a bit awkward:
- If there's HTML templating (e.g.
<pre>
and <code>
tags) that is required for each usage, that should be included in the the directive itself via the template
or templateUrl
properties rather than expecting the consumer to know that it's required
- You can probably get away implementing this without using
$compile
- you can either put the output of prettyPrintOne
on the scope, and just bind to it in the template specified in the template
or templateUrl
properties, or you can use jQuery to get a reference to whichever element will be the container (that is, if it's not the top-level element) and use html()
to set its content.
- If you do actually need to allow other templated HTML content to be defined inside the directive, look into the
transclude
option defined in the directive guide.