1

I'm working on a directive, where one of the parameters to it can either be a model (dynamic value) or sometimes a string.

The only way I could do this was using @. Is there a better way to do this?

<my-directive foo="{{foomodel}}">
<my-directive foo="foostring">

In my directive I'm using an isolate scope:

scope: {
    foo: '@'
}

I tried doing this with just strings and it did not work.

chovy
  • 72,281
  • 52
  • 227
  • 295

3 Answers3

2

Reading these answers it seems there is much confusion about the differences between '=', '&' and '@' when using an isolated scope. For your purposes, I would use & and if you want the value to be a string or an object do as @pankajparkar does and encase strings in single quotes.

Unless you need changes in your directive to be reflected in your parent scope, don't use '='. '=' implicitly sets a watch on the isolated scope's property and modifies the parent scope property if it sees a change. Watches are a relatively expensive operation in Angular, and you want to minimize them.

& does not implicity create a watch and it defers evaluation of the expression for when you call the created scope function. It is ideal for your case because you don't need two-way binding. Think of & as creating a function that returns the value generated by the expression assigned to the attribute.

app.directive("foo", function(){
  return {
    scope: {
      foo: "&"
    },
    template: "<span>{{foo()}}</span> | <span>{{typeof foo()}}</span>",
    link: function(scope){
        //so we don't have to set up a watch, I've just inlined the evaluation of the type
    }
  }
})

<div foo="model"></div>       <!-- test | string -->
<div foo="'text'"></div>      <!-- text | string -->
<div foo="5"></div>           <!-- 5 | number -->
<div foo="{a: 5}"></div>

@ is reserved for cases where you want to restrict evaluation of the expression to interpolation. Interpolation in angular ALWAYS results in a string. If you use '@' and your attribute is not an interpolation expression ({{}}), the value of the scope property will be the literal value of the attribute. If your attribute value is an interpolation expression, the value of the property will be the interpolated value of the expression (which will always be a string).

Clear as mud, right?

Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75
  • This is misuing the intent of `"&"`, not to mention that it creates a cumbersome function syntax. Instead, it's better to use `$parse(attrs.foo)(scope)` if watching for changes is not needed – New Dev Feb 25 '15 at 23:38
  • Why inject $parse when you don't need to? I do not agree with this comment at all. – Joe Enzminger Feb 25 '15 at 23:39
  • You are not losing anything by injecting `$parse`. You are losing on readibility with the function syntax everywhere by using `"&"`. Not to mention that `"&"` necessitates an isolate scope, where with `$parse` it doesn't – New Dev Feb 25 '15 at 23:40
  • I'm sorry, I just don't agree. – Joe Enzminger Feb 25 '15 at 23:45
  • The OP specified he was using an isloated scope (I'm assuming for good reason). By your logic you should not use '=', either, and instead manually implement two way binding with $parse and $watch? For what it's worth, the internal implementation of '&' is essentially "return function(locals) { return $parse(attr.value)(scope, locals) }" (modified for brevity). Not sure why you would bother doing by hand what angular will do for you. – Joe Enzminger Feb 25 '15 at 23:57
  • I provided the option for all the cases, isolate or not, two-way binding or one-time. From the [guide on directives](https://docs.angularjs.org/guide/directive): "`&` bindings are ideal for binding callback functions to directive behaviors". But, feel free to use it if you find this syntax intuitive to access objects: `model().prop`. – New Dev Feb 26 '15 at 00:13
1

Whenever you have string value then you should wrap it inside ' single quote, because @ means expression evaluation using $parse API of angular

HTML

<my-directive foo="'foostring'">

Hope this could help you, Thanks.

Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
0

If you are using isolate scope, then you could use the two-way binding: "=". This would take a model or a literal, in the following way (assuming $scope.model = "test":

<div foo="model"></div>       <!-- test | string -->
<div foo="'text'"></div>      <!-- text | string -->
<div foo="5"></div>           <!-- 5 | number -->
<div foo="{a: 5}"></div>      <!-- {"a":4} | object -->

and this is the directive definition:

app.directive("foo", function(){
  return {
    scope: {
      foo: "="
    },
    template: "<span>{{foo}}</span> | <span>{{type}}</span>",
    link: function(scope){
      scope.type = typeof scope.foo;
    }
  }
})

plunker

EDIT:

As per feedback from @JoeEnzminger, the "=" is an expensive way to get the actual object as it sets up 2 watchers (for a two-way binding).

For one-way binding (with a single watcher)

You could the foo: "@" binding. This will result in all of the values being interpreted as a string. Specifically, to pass the value of $scope.model you would need to use an expression: {{model}}.

Alternatively, as suggested by @JoeEnzminger's answer below, you could use the foo: "&" binding. This will allow to get the actual model/object back. The drawback is a more awkward function syntax {{foo()}} for expressions.

For one-time binding:

If you could do away with one-time binding, then you could use the $parse service to get the actual object.

For isolate scope:

scope: {}, 
link: function(scope, element, attrs){
  scope.foo = $parse(attrs.foo)(scope.$parent);
}

For inherited scope:

scope: true,
link: function(scope, element, attrs){
  scope.foo = $parse(attrs.foo)(scope);
}
Community
  • 1
  • 1
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • I'm curious why you think $parse is more appropriate. And I'd like to see where you got the idea that my solution misuses '&'. – Joe Enzminger Feb 25 '15 at 23:44
  • @JoeEnzminger, I will revise it. I meant to say "misusing the intent of `"&"` - not "misusing" – New Dev Feb 26 '15 at 00:18
  • See [this Plunk](http://plnkr.co/edit/bzRToBH8y3ifp0b4cVrm) for why your edit above are probably not doing what you want it to do (changes in the model will not be reflected in "foo". – Joe Enzminger Feb 26 '15 at 00:25
  • @JoeEnzminger, I thought your whole point was that you didn't want to setup a $watch. What have you achieved then? – New Dev Feb 26 '15 at 00:27
  • With '=', you set up two watches - one watching the parent scope value and modifying the directive scope on changes, and the other binding your directive scope value to the template. & eliminates the first watch(). If you are going to use the value of scope.foo in a template (regardless of how it is expressed), you are going to generate a watch. I am not claiming you can eliminate that. My point is don't use '=' if you do not need two way binding. – Joe Enzminger Feb 26 '15 at 00:34
  • @JoeEnzminger, True, but you're invoking an expensive `$parse` call every time you use `{{foo()}}`, whereas the parent watch does it only once per digest. So, if you used `{{foo()}}` once, then `"&"` wins on perf but loses on readability. Any more than that and I think any perf gain goes away. – New Dev Feb 26 '15 at 02:21
  • I've modified the Plunk so it more closely mirrors what '&' actually does, so it doesn't call $parse on every invocation of foo(). '&' only calls $parse once as well, not every $digest(). – Joe Enzminger Feb 26 '15 at 02:27
  • I take the last statement back - `{{foo()}}` doesn't call `$parse` on every digest. Indeed, for one-way bound with the ability to get the actual object (as opposed to string), `"&"` seems to be the way to go, however ugly it is. – New Dev Feb 26 '15 at 03:27