Using the directive inside ng-repeat and the mysterious power of visibility "@",

If you prefer to see the question in the working code, start here: http://jsbin.com/ayigub/2/edit

Consider these almost equivalent ways of writing a simple pointer:

app.directive("drinkShortcut", function() { return { scope: { flavor: '@'}, template: '<div>{{flavor}}</div>' }; }); app.directive("drinkLonghand", function() { return { scope: {}, template: '<div>{{flavor}}</div>', link: function(scope, element, attrs) { scope.flavor = attrs.flavor; } }; }); 

When used on their own, the two directives work and behave the same:

  <!-- This works --> <div drink-shortcut flavor="blueberry"></div> <hr/> <!-- This works --> <div drink-longhand flavor="strawberry"></div> <hr/> 

However, when used in ng-repeat, only the shortcut version works:

  <!-- Using the shortcut inside a repeat also works --> <div ng-repeat="flav in ['cherry', 'grape']"> <div drink-shortcut flavor="{{flav}}"></div> </div> <hr/> <!-- HOWEVER: using the longhand inside a repeat DOESN'T WORK --> <div ng-repeat="flav in ['cherry', 'grape']"> <div drink-longhand flavor="{{flav}}"></div> </div> 

My questions:

  • Why is the longhand version not working inside ng-repeat?
  • How could you do work with the long version inside ng-repeat?
+60
javascript angularjs angularjs-directive
May 11 '13 at 23:09
source share
1 answer

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

+99
May 11 '13 at 11:59
source share



All Articles