In drinkLonghand you use code
scope.flavor = attrs.flavor;
During the binding phase, the interpolated attributes have not yet been evaluated, so their values โโare undefined . (They work outside of ng-repeat , because in those cases you do not use string interpolation, you just go through a normal regular string, such as "strawberries.") This is mentioned in the Developer's Guide for Developers , as well as the Attributes method, which is not in the API documentation called $observe :
Use $observe to observe changes in attribute values โโthat contain interpolation (for example, src="{{bar}}" ). Not only is this very efficient, but it is also the only way to easily get the actual value, because during the snap phase the interpolation has not yet been evaluated, and therefore the value at this time is 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 is that it does not use the isolation area; so the line
scope.flavor = flavor;
has the ability to overwrite an existing variable in an area called flavor . Adding an empty selection area also does not work; this is because Angular is trying to interpolate the string based on the scope of the directive that does not have the flav attribute flav . (You can verify this by adding scope.flav = 'test'; on the call to attrs.$observe .)
Of course, you can fix this by defining the selection area, for example
scope: { flav: '@flavor' }
or by creating an uninsulated content area
scope: true
or without relying on the template on {{flavor}} and instead do some direct manipulation of the DOM, such as
attrs.$observe('flavor', function(flavor) { element.text(flavor); });
but this defeats the goal of the exercise (for example, it would be easier to just use the drinkShortcut method). So, to make this directive, we will rip out the $interpolate service to do the interpolation on our own in the scope of the $parent directive:
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 can change, you should use $watch and reevaluate the result of the interpolation function fn (I'm not positive from my head, as you know what to do with $watch , you just need to pass the function). scope: { flavor: '@' } is a good shortcut to avoid having to deal with all this complexity.
[Update]
To answer the question from the comments:
How does the shortcut method solve this problem behind the scenes? Does it use the $ interpolation service like you do, or does something else?
I was not sure about this, so I looked at 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; }
Thus, it seems that attrs.$observe can internally use a region other than the current one to base the observation of the attribute (next to the last line, above break ). Although it may be tempting to use this on your own, keep in mind that anything with a two-dollar prefix $$ should be considered private to the Angular private API and may change without warning (not to mention that you get it for any case when using @ mode).