In drinkLonghand
, you use the code
scope.flavor = attrs.flavor;
During the linking phase, interpolated attributes haven't yet been evaluated, so their values are undefined
. (They work outside of the ng-repeat
because in those instances you aren't using string interpolation; you're just passing in a regular ordinary string, e.g. "strawberry".) This is mentioned in the Directives developer guide, along with a method on Attributes
that is not present in the API documentation called $observe
:
Use $observe
to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"
). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined
.
So, to fix this problem, your drinkLonghand
directive should look like this:
app.directive("drinkLonghand", function() {
return {
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
attrs.$observe('flavor', function(flavor) {
scope.flavor = flavor;
});
}
};
});
However, the problem with this is that it doesn't use an isolate scope; thus, the line
scope.flavor = flavor;
has the potential to overwrite a pre-existing variable on the scope named flavor
. Adding a blank isolate scope also doesn't work; this is because Angular attempts to interpolate the string on based on the directive's scope, upon which there is no attribute called flav
. (You can test this by adding scope.flav = 'test';
above the call to attrs.$observe
.)
Of course, you could fix this with an isolate scope definition like
scope: { flav: '@flavor' }
or by creating a non-isolate child scope
scope: true
or by not relying on a template
with {{flavor}}
and instead do some direct DOM manipulation like
attrs.$observe('flavor', function(flavor) {
element.text(flavor);
});
but that defeats the purpose of the exercise (e.g. it'd be easier to just use the drinkShortcut
method). So, to make this directive work, we'll break out the $interpolate
service to do the interpolation ourself on the directive's $parent
scope:
app.directive("drinkLonghand", function($interpolate) {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
// element.attr('flavor') == '{{flav}}'
// `flav` is defined on `scope.$parent` from the ng-repeat
var fn = $interpolate(element.attr('flavor'));
scope.flavor = fn(scope.$parent);
}
};
});
Of course, this only works for the initial value of scope.$parent.flav
; if the value is able to change, you'd have to use $watch
and reevaluate the result of the interpolate function fn
(I'm not positive off the top of my head how you'd know what to $watch
; you might just have to pass in a function). scope: { flavor: '@' }
is a nice shortcut to avoid having to manage all this complexity.
[Update]
To answer the question from the comments:
How is the shortcut method solving this problem behind the scenes? Is it using the $interpolate service as you did, or is it doing something else?
I wasn't sure about this, so I looked in the source. I found the following in compile.js
:
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
var match = definiton.match(LOCAL_REGEXP) || [],
attrName = match[2]|| scopeName,
mode = match[1], // @, =, or &
lastValue,
parentGet, parentSet;
switch (mode) {
case '@': {
attrs.$observe(attrName, function(value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = parentScope;
break;
}
So it seems that attrs.$observe
can be told internally to use a different scope than the current one to base the attribute observation on (the next to last line, above the break
). While it may be tempting to use this yourself, keep in mind that anything with the double-dollar $$
prefix should be considered private to Angular's private API, and is subject to change without warning (not to mention you get this for free anyway when using the @
mode).