You Cannot Link Attribute Interpolation Multiple Times In AngularJS
Once you have a compiled directive, in AngularJS, you can either link it to the current scope; or, you can clone the compiled template and link each new clone to a different scope. But, what if we didn't want to clone the template? What if we just wanted to move it around in the DOM (Document Object Model)? Could we simply re-link it to different scopes? From what I see, the answer depends on the content of the template. It appears that attribute interpolation does not play nicely with re-linking.
Run this demo in my JavaScript Demos project on GitHub.
To test this approach, I've created an ngRepeat list that doesn't have any actual content. But, it does have a "bnFriend" directive. This directive will pre-compile a template and then attempt to inject and link said template into each ngRepeat as the user mouses into the list element. Since I am not cloning the template, each .append() call will implicitly remove the template from its former parent node.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
You Cannot Link Attribute Interpolation Multiple Times In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
You Cannot Link Attribute Interpolation Multiple Times In AngularJS
</h1>
<ul class="friends">
<li
ng-repeat="friend in friends"
bn-friend
class="friend">
<!-- WE WILL BE COMPILING THIS LATER ON IN THE DIRECTIVE BELOW. -
<div title="{{ friend.name }}" class="bio">
<img ng-src="./avatar-{{ friend.avatarID }}.jpg" />
<div class="name">{{ friend.name }}</div>
<div class="nickname">{{ friend.nickname }}</div>
</div>
-->
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="./angular-1.2.26.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// Notice that each friend has a different avatarID - this will be used,
// in conjunction with attribute interpolation (ngSrc), to render a
// unique avatar image for each friend in the ngRepat.
$scope.friends = [
{
avatarID: 1,
name: "Sarah",
nickname: "Stubs"
},
{
avatarID: 2,
name: "Joanna",
nickname: "J-Diesel"
},
{
avatarID: 3,
name: "Tricia",
nickname: "Boss"
}
];
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I attempt to compile a template once and then re-link it in various contexts
// depending on the user's mouse interactions.
// --
// CAUTION: THIS APPROACH DOES NOT WORK - THIS DEMO IS TO LOOK AT WHY IT
// DOESN'T WORK BASED ON HOW THE ATTRIBUTE INTERPOLATION IS RE-COMPILED DURING
// EACH PRE-LINKING PHASE.
app.directive(
"bnFriend",
function( $compile ) {
// Compile the template that we want to inject in each context.
var templateLinker = $compile(
"<div title='{{ friend.name }}' class='bio'>" +
"<img ng-src='./avatar-{{ friend.avatarID }}.jpg' />" +
"<div class='name'>{{ friend.name }}</div>" +
"<div class='nickname'>{{ friend.nickname }}</div>" +
"</div>"
);
// I will keep track of the element as it is injected into each context.
// Note that it will only be in a single context at a time since we are
// NOT cloning it - simply moving it and re-linking it.
var template = null;
// I hold the scope that gets created a linked to the above template. We
// have to create a new scope for the template so that we have something
// we can $destroy() when we unlink it.
var templateScope = null;
// I link the JavaScript events to the scope.
function link( $scope, element, attributes ) {
// When the user mouses-into the ngRepeat item, we're going to re-
// link the above template and inject it into the current ngRepeat
// context.
element.on(
"mouseenter",
function handleMouseEnterEvent( event ) {
// If this is not the first time we've linked this template,
// destroy the previous context.
if ( templateScope ) {
templateScope.$destroy();
}
// Link the template to a NEW, child scope.
// --
// CAUTION: THIS DOES NOT WORK AS EXPECTED WITH ATTRIBUTE
// INTERPOLATION.
template = templateLinker( templateScope = $scope.$new() );
// Inject the template - this automatically removes it from
// the previous DOM context since we're not cloning it - we're
// just moving it around.
element.append( template );
// Make sure all the watchers fire on the DOM tree we just
// injected.
templateScope.$digest();
}
)
}
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
}
);
</script>
</body>
</html>
This demo uses its own version of the AngularJS library that I have augmented with some logging. Specifically, I have added log statements to the Text Interpolation and the Attribute Interpolation directives so that I can see when they fire and what state they are in at link time.
When I mouse into the first list item, we get the following console output:
Attribute Interpolation Prelinking on title
>>> Original value: {{ friend.name }}
>>> Current value: {{ friend.name }}
Attribute Interpolation Prelinking on ngSrc
>>> Original value: ./avatar-{{ friend.avatarID }}.jpg
>>> Current value: ./avatar-{{ friend.avatarID }}.jpg
Text Interpolation Linking on {{ friend.name }}
Text Interpolation Linking on {{ friend.nickname }}
As you can see, the attribute interpolation sees the {{}} syntax markers.
Now, when I mouse out of the first list item and into the second list item, we get the following console output when the compiled template is re-linked to the new scope:
$destroy called on: 009 with 4 watchers.
Attribute Interpolation Prelinking on title
>>> Original value: {{ friend.name }}
>>> Current value: Sarah
>>> CAUTION: No interpolation requirements found in attribute, Sarah
Attribute Interpolation Prelinking on ngSrc
>>> Original value: ./avatar-{{ friend.avatarID }}.jpg
>>> Current value: ./avatar-1.jpg
>>> CAUTION: No interpolation requirements found in attribute, ./avatar-1.jpg
Text Interpolation Linking on {{ friend.name }}
Text Interpolation Linking on {{ friend.nickname }}
Notice that this time the "current value" of the attribute doesn't contain the {{}} interpolation markers. As such, the second pre-linking phase doesn't see any need for interpolation and therefore does not bind any watchers for attribute interpolation. This is why the attribute interpolation works on the first list item, but not on any of the subsequent list items.
Notice, however, that the Text Interpolation continues to work on each list item. This is because the text interpolation link function does not get recomputed for each linking phase. As such, it uses the original template value on each subsequent linking action.
If you look at the source code for the attribute interpolation directive, there is a note on why they recompute the linking function:
// we need to interpolate again, in case the attribute value has been updated
// (e.g. by another directive's compile function)
I think this is a byproduct of the order in which attribute interpolation is applied in the context of other directives. But, the AngularJS source code for compiling HTML is, what one might call, "complicated". As such, I can't make any confident statements about it. But, from my testing, it seems re-linking a compiled HTML template in different contexts is going to be a dangerous move. Probably, it's just better to clone the template and then link it.
Want to use code from this post? Check out the license.
Reader Comments