Deepak Nulu Deepak Nulu - 5 months ago 36
AngularJS Question

Directive isolate scope with ng-repeat scope in AngularJS

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?


Answer

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