95

I have a directive with an isolate-scope (so that I can reuse the directive in other places), and when I use this directive with an ng-repeat, it fails to work.

I have read all the documentation and Stack Overflow answers on this topic and understand the issues. I believe I have avoided all the usual gotchas.

So I understand that my code fails because of the scope created by the ng-repeat directive. My own directive creates an isolate-scope and does a two-way data-binding to an object in the parent scope. My directive will assign a new object-value to this bound variable and this works perfectly when my directive is used without ng-repeat (the parent variable is updated correctly). However, with ng-repeat, the assignment creates a new variable in the ng-repeat scope and the parent variable does not see the change. All this is as expected based on what I have read.

I have also read that when there are multiple directives on a given element, only one scope is created. And that a priority can be set in each directive to define the order in which the directives are applied; the directives are sorted by priority and then their compile functions are called (search for the word priority at http://docs.angularjs.org/guide/directive).

So I was hoping I could use priority to make sure that my directive runs first and ends up creating an isolate-scope, and when ng-repeat runs, it re-uses the isolate-scope instead of creating a scope that prototypically inherits from the parent scope. The ng-repeat documentation states that that directive runs at priority level 1000. It is not clear whether 1 is a higher priority level or a lower priority level. When I used priority level 1 in my directive, it did not make a difference, so I tried 2000. But that makes things worse: my two-way bindings become undefined and my directive does not display anything.

I have created a fiddle to show my issue. I have commented out the priority setting in my directive. I have a list of name objects and a directive called name-row that shows the first and last name fields in the name object. When a displayed name is clicked, I want it to set a selected variable in the main scope. The array of names, the selected variable are passed to the name-row directive using two-way data-binding.

I know how to get this to work by calling functions in the main scope. I also know that if selected is inside another object, and I bind to the outer object, things would work. But I am not interested in those solutions at the moment.

Instead, the questions I have are:

  • How do I prevent ng-repeat from creating a scope that prototypically inherits from the parent scope, and instead have it use my directive's isolate-scope?
  • Why is priority level 2000 in my directive not working?
  • Using Batarang, is it possible to know what type of scope is in use?
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Deepak Nulu
  • 961
  • 1
  • 7
  • 6
  • 1
    Normally, you don't want to use an isolate scope if your directive will be used on the same element with other directives. Since you are creating your own scope properties, and you need to work with ng-repeat, I suggest using `scope: true` for your directive. See also (if you haven't already) http://stackoverflow.com/questions/14914213/when-writing-a-directive-how-do-i-decide-if-a-need-no-new-scope-a-new-child-sc Also, just because a directive will be used in multiple places does not mean we should automatically use an isolate scope. – Mark Rajcok Mar 25 '13 at 22:13
  • I have read many of your answers (they are beyond excellent, thanks for writing them), but it never occurred to me to read your questions :-). I read what you linked to. It appears to me that isolate-scope directives cannot be mixed with other directives. I agree with the sentiment that such directives are components and therefore they do not need to be mixed with other directives. The one exception (so far) for me would be `ng-repeat`. I think it is valuable to be able to mix standalone directives with `ng-repeat`. To be continued... – Deepak Nulu Mar 25 '13 at 23:15
  • Continued from above... So if there should be only one directive with a scope for an element, then `ng-repeat` should not have a scope. `ng-repeat` having a scope does make sense for the typical use-case, so I am not suggesting it be changed. Instead, like I commented in Alex Osborn's answer, I think I will create a repeat directive based on `ng-repeat` that does not create its own scope. This can then be used for repeating directives which have their own isolate-scopes. To be continued... – Deepak Nulu Mar 25 '13 at 23:19
  • The code that repeats a directive now needs to know whether to use `ng-repeat` or the custom scope-less repeat directive. I think it is okay for the "caller" to know this, but it is not okay for a "callee" (the directive being repeated) to know whether it is being repeated or not. To be continued... – Deepak Nulu Mar 25 '13 at 23:29
  • Getting a little crazy with the comments here... :-) ngRepeat *must* create its own scope. Why do you feel you need an isolate scope here? – Josh David Miller Mar 25 '13 at 23:30
  • Also, I think it is important for a component (directive) to be able to change in the future without breaking the code that uses it, especially if the component is just adding behavior without changing its "API" (i.e. it does not need additional bindings to support the new behavior). The new behavior might need an isolate-scope where none was needed before in order to store component-specific state. So I think it is not ideal to have to decide what type of scope to use for a "component directive" based on the current usage patterns of that component. – Deepak Nulu Mar 25 '13 at 23:32
  • @JoshDavidMiller sorry for the firehose of comments :-). My first time using stackoverflow; maybe this would have been better off in the AngularJS Google Group. I made the smallest possible fiddle to show I could not mix my directive with `ng-repeat`. My actual code has directive-specific state that I do want to isolate from the parent scope. Do you think I will be able to create my own variant of `ng-repeat` that uses another directive's scope, or is there something fundamental in `ng-repeat` that cannot work with another directive? I am a JS newb & have not taken a look at Angular code yet. – Deepak Nulu Mar 25 '13 at 23:39
  • ngRepeat works by creating a set of DOM elements, adding a scope to each, and then creating some variable on each scope containing the iterated value and some metadata. It can't work without a separate scope. If you wrote your own, it would be error-prone - it would require that there was already a scope created by a higher-priority directive, and then your new repeater would *pollute* that scope. So much for isolation! That said, you *can* [use ngRepeat and an isolate scope](http://plnkr.co/edit/Vd5xjt49nEHHT4ZWLvzT?p=preview), so I'm not sure what the issue is. – Josh David Miller Mar 25 '13 at 23:56
  • My issue: Writing to a 2-way data binding in a custom directive with isolate-scope fails with `ng-repeat`. Your plunker uses 2-way data binding, but it only reads the bound variable; it does not write to a bound variable. I don't need to write to the variable pointing to the current iteration (`val` in your plunker), but I have other variables that I bind to in my directive's isolate-scope and I need to be able to modify those variables from within my directive. For example, in my fiddle (linked to in my question), I need to modify `ioSelected`, and that does not work when I use `ng-repeat`. – Deepak Nulu Mar 26 '13 at 01:38
  • Oh, well then the answer is even simpler. Use `io-selected="$parent.selected"`. – Josh David Miller Mar 26 '13 at 04:59
  • But also keep in mind that your issue is not with ngRepeat but with the way prototypical inheritance works in JavaScript. The issue is that you're using a primitive. Model values should *always* have a `.` in AngularJS. – Josh David Miller Mar 26 '13 at 05:01
  • @DeepakNulu I think you are creating a directive where one is not necessary. Seemingly what you are attempting to do can be achieved by a combination of ng-include and ng-repeat. Put in a Controller for each of the repeated items and you are good to go. – ganaraj Mar 26 '13 at 13:32
  • @JoshDavidMiller I thought the use of `$parent` is a code-smell. What if my directive is nested in multiple `ng-repeat` directives? Then my directive would have to know how deep the rabbit hole goes and chain a whole bunch of `$parent.$parent...`, right? `ng-repeat` introduces a scope because it needs to make the current iteration available to each repetition. But what if I have a repeat variant that binds the current iteration to an input in the directive? Then the repeat directive does not need to introduce a scope at all. But I need to look into the metadata you said `ng-repeat` needs. – Deepak Nulu Mar 26 '13 at 16:37
  • @ganaraj my fiddle is the smallest example to illustrate I cannot write to a 2-way binding when I use `ng-repeat`. I like the concept of directives (and 2-way data binding) and I have an extremely minimal standalone controller in my (full-page) app (I am hoping I will be able to do away with this controller, but I digress). So I am looking for a way to make my directives work in any scenario with 2-way data binding. – Deepak Nulu Mar 26 '13 at 16:55
  • Can someone help me with this one? http://stackoverflow.com/questions/22000201/angularjs-directive-with-isolate-scope-ng-repeat – user1791567 Feb 25 '14 at 18:56

2 Answers2

203

Okay, through a lot of the comments above, I have discovered the confusion. First, a couple of points of clarification:

  • ngRepeat does not affect your chosen isolate scope
  • the parameters passed into ngRepeat for use on your directive's attributes do use a prototypically-inherited scope
  • the reason your directive doesn't work has nothing to do with the isolate scope

Here's an example of the same code but with the directive removed:

<li ng-repeat="name in names"
    ng-class="{ active: $index == selected }"
    ng-click="selected = $index">
    {{$index}}: {{name.first}} {{name.last}}
</li>

Here is a JSFiddle demonstrating that it won't work. You get the exact same results as in your directive.

Why doesn't it work? Because scopes in AngularJS use prototypical inheritance. The value selected on your parent scope is a primitive. In JavaScript, this means that it will be overwritten when a child sets the same value. There is a golden rule in AngularJS scopes: model values should always have a . in them. That is, they should never be primitives. See this SO answer for more information.


Here is a picture of what the scopes initially look like.

enter image description here

After clicking the first item, the scopes now look like this:

enter image description here

Notice that a new selected property was created on the ngRepeat scope. The controller scope 003 was not altered.

You can probably guess what happens when we click on the second item:

enter image description here


So your issue is actually not caused by ngRepeat at all - it's caused by breaking a golden rule in AngularJS. The way to fix it is to simply use an object property:

$scope.state = { selected: undefined };
<li ng-repeat="name in names"
    ng-class="{ active: $index == state.selected }"
    ng-click="state.selected = $index">
    {{$index}}: {{name.first}} {{name.last}}
</li>

Here is a second JSFiddle showing this works too.

Here is what the scopes look like initially:

enter image description here

After clicking the first item:

enter image description here

Here, the controller scope is being affected, as desired.

Also, to prove that this will still work with your directive with an isolate scope (because, again, this has nothing to do with your problem), here is a JSFiddle for that too, the view must reflect the object. You'll note that the only necessary change was to use an object instead of a primitive.

Scopes initially:

enter image description here

Scopes after clicking on the first item:

enter image description here

To conclude: once again, your issue isn't with the isolate scope and it isn't with how ngRepeat works. Your problem is that you're breaking a rule that is known to lead to this very problem. Models in AngularJS should always have a ..

Community
  • 1
  • 1
Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • My experiments with directives with isolate-scopes and 2-way data binding lead me to believe that the `.` golden rule is only required for scopes that have prototypical inheritance. With isolate-scopes, the variables you are interested in are defined in the `scope: {}` definition in the directive, and therefore those variables will exist in the isolate-scope from the beginning. Also, they don't mask the parent variables because the isolate-scope does not prototypically inherit from the parent scope. i.e. there is no "parent" scope to mask. – Deepak Nulu Mar 26 '13 at 20:31
  • But updating a two-way bound variable in the isolate-scope (defined with `=` in `scope: {}`) causes the corresponding variable in the outer scope to change because of the two-way data binding. I can't remember trying this out with primitive variables in the isolate-scope, but I have definitely seen this working with object/reference variables. Note that I am talking about assigning a new object value to the variable, not modifying a property inside the object. i.e. I am not following the `.` golden rule in my directive and it still works. – Deepak Nulu Mar 26 '13 at 20:32
  • I have forked my fiddle and replaced the `ng-repeat` with a manual repetition: http://jsfiddle.net/jtjk4/1/ (I created variables for each index in `MainController` because my directive uses an `inIndex : '='` binding for the index). You will see that this works fine. My directive assigns a new value to `ioSelected` without using `.` and it works correctly (the outer scope sees the change and displays the selected name in the HTML output). This is what leads me to say that it is `ng-repeat` (specifically its scope) that breaks my two-way data-bound isolate-scope directive. What am I missing? – Deepak Nulu Mar 26 '13 at 20:33
  • I also have cases where I have nested directives, all with isolate-scopes and two-way data-binding in them. The values bound to my outer-directive need to be passed to my inner-directive. Requiring a wrapper object (so that I can follow the `.` golden rule) becomes a leaky abstraction that forces me to rewrite my directives. – Deepak Nulu Mar 26 '13 at 20:33
  • To me, this means the promise of directives as standalone components is broken. Please note that I am aware of all the solutions mentioned so far and I am passing functions to my directives to overcome this issue. But I feel very strongly that there needs to be a better solution that will let us realize the full potential of directives and two-way data binding. **** – Deepak Nulu Mar 26 '13 at 20:34
  • It really has **nothing** to do with your isolate scope or with your directive. Nothing. ngRepeat **must** create its own scope to work properly; **so will any number of other components**. Meaning even if you wrote your own frailer version of ngRepeat to force it to work for you in your case, it will similarly break in **plenty** of other cases (e.g. transclusion). And you're doing it just to break a rule! In AngularJS, you *always* include a `.`. When you don't, you're making it frail and it will eventually stop working. What I really don't get is why you're resisting it in the first place. – Josh David Miller Mar 26 '13 at 20:52
  • 1
    Should have also said: *your* directive isn't creating a child scope, but it could easily be *used* in a context that *requires* a child scope. ngRepeat is one case. So is transclusion. Trust me - use the `.`. – Josh David Miller Mar 26 '13 at 20:54
  • 1
    [Discussion on the AngularJS Google Group](https://groups.google.com/d/msg/angular/5-MXXOszkJQ/TxPjhKgO2o4J) where @JoshDavidMiller was finally able to clear my confusion! – Deepak Nulu Mar 27 '13 at 00:29
  • How did you guys make those charts? they are awesome – Jeff Voss Jan 16 '14 at 19:30
  • 2
    Where do you guys get all these cool diagrams from? I've seen another user posting these as well. – Asad Saeeduddin Apr 08 '14 at 21:29
  • 3
    @Asad, I just recently put the tool up on GitHub, it's called [Peri$scope](https://github.com/mrajcok/angularjs-periscope). – Mark Rajcok Apr 03 '15 at 04:41
6

Without directly trying to avoid answering your questions, instead take a look at the following fiddle:

http://jsfiddle.net/dVPLM/

Key point is that instead of trying to fight and change the conventional behaviour of Angular, you could structure your directive to work with ng-repeat as opposed to trying to override it.

In your template:

    <name-row 
        in-names-list="names"
        io-selected="selected">
    </name-row>

In your directive:

    template:
'        <ul>' +      
'            <li ng-repeat="name in inNamesList" ng-class="activeClass($index)" >' +
'                <a ng-click="setSelected($index)">' +
'                    {{$index}} - {{name.first}} {{name.last}}' +
'                </a>' +
'            </li>' +
'        </ul>'

In response to your questions:

Community
  • 1
  • 1
Alex Osborn
  • 9,831
  • 3
  • 33
  • 44
  • Thank you for such a quick response. If what I am attempting goes against the grain, I am a bit saddened (AngularJS is nonetheless an awesome framework). A directive is meant to be a reusable component. When it is written, the author should not have to worry about whether it will be used with an `ng-repeat` or not. Maybe when it is first written, it is never used with `ng-repeat`. And some time in the future, it might get used with `ng-repeat`, and at that point in time, it should just work without a rewrite. Hopefully a future release of AngularJS will make this possible. – Deepak Nulu Mar 25 '13 at 21:27
  • I would like to clarify my comment above. I think it is possible to write a directive that does not worry about whether it is being used with `ng-repeat` or not. But it appears that I would have to pass in a function to the directive so that it can modify the variables in the parent scope, instead of being able to mutate a two-way binding in the directive's own scope. Two-way binding and reusable directives are my top two favorite things about AngularJS and `ng-repeat` is proving to be a fly in the ointment. Maybe I can write an `ng-repeat` equivalent that does not create its own scope. – Deepak Nulu Mar 25 '13 at 22:38