2

This has come up for me in answers and generated some discussion and I'd like to figure out if I'm way off base or if there is a "more angular" way to accomplish my goal.

When I write a directive that is going to use an isolated scope, a question that comes up is always =, & or @.

Generally, people always think of & as a way to pass functions to directive. The documentation describes it as:

a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

I use it for passing functions quite a bit, but I also use it in cases where I want to pass an expression that returns a non-string, and I don't want my directive to be able to modify values in the parent scope. In other words, I use it as:

a way to execute an expression in the context of the parent scope

So if I do not need two-way binding, and the expression is not a string, I will do this:

.directive('displayObject', function() {

    scope: {
        value: '&=displayObject'
    },
    template: '<div ng-repeat="(k, v) in value()">{{k}}: {{v}}</div>',
    replace: true,
    ...
});

The directive usage would be:

<div displayObject="someScopePropertyOrExpression"></div>

= isn't ideal here because I do not need two way binding. I don't want my directive modifying the value in the parent scope and I don't want the watch required to maintain it.

@ isn't ideal because it interpolates the attribute, so value would always be a string.

::someScopePropertyOrExpression doesn't work because I want the directive template to reflect changes in someScopePropertyOrExpression if it changes.

In each discussion, it's always brought up that

ng-repeat="(key, value) in value()"

sets up a watch - the problem is that = and the template together set up two - one that is wholly unnecessary.

Several times when I've suggested this pattern it's been called a "hack", or a misuse of &, and even "ugly".

I don't think it is any of these, but if it is, then my specific question is what is the alternative?

Community
  • 1
  • 1
Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75

1 Answers1

0

I've searched quite a bit to prove you wrong in our discussion here, but I think you are right - this is the only way to set up a one-way (from parent to isolate scope) binding to an actual model.

In that aspect "&" is superior to "=", because, as you noted, "=" sets up a watch on the parent's scope whereas "&" does not.

One thing - plunker 1 - (that I finally managed to uncover) that hints on it being a hack is that it does not play nice with Angular's one-time binding foo="::name":

"=" will honor the one-time binding (it sets up a watch on the parent and removes it before the next digest), whereas "&" would not:

<input ng-model="name">
<div two-way="::name">
<div one-way="::name">

The other thing - plunker 2 - is that "&" allows to pass local variables, and they may shadow outer variables:

<input ng-model="name">
<div one-way="name">

But in the directive if you did something like: "{{oneWay({name: 'fooo'})}}", it will cause it to display "fooo", rather than take the value of name from the outer scope. It's a bit like shooting yourself in the foot, yes, but it does hint that this usage might be slightly outside of its original intent.

Community
  • 1
  • 1
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Thanks for answering NewDev. If you want one time binding, the ='::name' is the way to go. But in my case I don't. I want to maintain the parent->directive binding (directive updates when the value of the expression changes). If all you needed was one time binding, then I agree, ='::name' would be better than [the equivalent](http://plnkr.co/edit/vyF0HW2m0JC6ugHYS85J?p=preview) & solution from a readability perspective. Also, remember that :: is a new feature in 1.3+. – Joe Enzminger Mar 11 '15 at 15:59
  • On your second issue, I think the ability to pass locals is actually more of a "hack". Look at [this plunk](http://plnkr.co/edit/sbkAMFCAmlI4TgIK1I7e?p=preview). Using "baz", if the consumer of the directive doesn't know the name of the local that the directive uses to pass data, unexpected results occur. baz2 is actually what I prefer because it makes the directive interface cleaner (details of the interface are no longer buried in the template). – Joe Enzminger Mar 11 '15 at 16:12
  • @JoeEnzminger, on the first, my point was that it is the prerogative of the directive user (not the author) to decide to do one-time binding, which would work everywhere, except with this implementation. So, "you" in this case is not you-the-author, so you can't claim that you just want to maintain "parent->directive binding" – New Dev Mar 11 '15 at 16:51
  • @JoeEnzminger, on the second, actually, `baz2` (if it was a real function call) seems very wrong to me. It couples your controller function, which should stand on its own, with what the View wants. In other words, if I wanted to do: `baz="doSomething(item, prop, '123')"`, it then leaves the flexibility to the author of the View about how to wire with View with the ViewModel, including what to do with data (`prop`) returned from the directive. But is neither here nor there. In point 2, it is actually the call of the directive author, so that is what I meant about "shooting yourself in the foot" – New Dev Mar 11 '15 at 16:58
  • Also, on second... this cannot be a hack if built-in directives, like `ng-click` and `ng-keypress` return a local variable `$event` – New Dev Mar 11 '15 at 17:06
  • That's kind of an apples to oranges comparison. the ngEventDirs don't use isolate scope (in fact, none of the built in angular directives uses scope: {} - if they need to isolate a scope, they do it by hand). In addition, the pattern they follow is more like dependency injection. The pattern I have a problem with is calling a function on the parent scope and overriding locals that may be scope variables on the parent scope as well. Leads to much confusion. – Joe Enzminger Mar 11 '15 at 19:26
  • @JoeEnzminger, well, from the point-of-view of the directive user it's the same. But even so, the [directive guide](https://docs.angularjs.org/guide/directive#creating-a-directive-that-wraps-other-elements) calls for this kind of usage. But in case, this is not really relevant to the topic here, except to show that this leads to *some* minor awkwardness and suggests (maybe) less-than-stellar practice. More though, on point #1 - this is really where it hints that this usage of `"&"` is somewhat odd. – New Dev Mar 11 '15 at 19:37
  • @JoeEnzminger, this usage of `"&"` notation has always felt weird to me, but I couldn't quite place my finger on it, until I heard Misko [explaining it better](https://www.youtube.com/watch?list=PLOETEcp3DkCoNnlhE-7fovYvqwVPrRiY7&t=856&v=-dMBcqwvYA0). The difference is symantic - of statements vs. expressions - and although in this case the user (not the author) of the directive is led to believe that he is using an expression and all will be fine, this is what has always bugged me about this approach. I still agree that it is superior to avoid a watcher, but it is symantically awkward – New Dev Mar 29 '15 at 04:13