Please help me understand scopes in AngularJS.
If I associate a controller within a directive (as opposed to within html), is it supposed to have any impact on the scope associated with the directive ?
How can I use ng-repeat after scope isolation ?
For e.g. here is an example: http://plnkr.co/edit/0flo5mru61r9h3H8kiW5?p=preview
ex1. If I comment out (div ng-controller="Ctrl")[line 40, 43] and instead uncomment (// controller: 'Ctrl')[line 35] within the directive, why aren't the same scopes/hierarchy created (as viewed in Batarang).
ex2. How can I run ng-repeat for instructorList and profList (separately) without changing the current controller and only playing around with the scope ?
I am not sure how to inspect plunkers in batarang, but.
If you do this, you're instantiating the controller twice: once on each directive element. Each time you instantiate it, you're creating a new scope. As such, you have two separate sibling scopes. You can see just from looking at the html that the heirarchy won't be the same as if you had them both within the same element that has its own scope. In the latter case, changes made by child element 1 would affect the same scope used by child element two.
It's not very clear what you mean here. ng-repeat should be done in html. You could put it in the template like this:
template: '<label ng-repeat="person in teacherList">{{person.id}}<input ng-model="person.name"><br></label>'
See this
Related
I am trying to understand a working code. It can build a very simple json data by adding name:value pairs one by one with GUI; by a custom directive and its link function, it builds a html template as the right hand of the image below:
What puzzles me is ng-model="$parent.keyName" in the highlighted part, as well as $parent.valueType, ng-model="$parent.valueName" in other part.
1) What does the $parent refer to (in the code or in the example of the above image)?
2) Is there a way to show the value of $parent or $parent.keyName in the console or by adding something (e.g., alert) in the program?
1) $parent refers to the parent scope (of any given scope). All scopes have a parent apart from $rootScope which is the top level scope. They can be created by any angular ng-* directive, including but not limited to ng-controller.
Generally speaking, it's considered bad practice to use $parent because it refers to the immediate parent scope which is subject to change. If the scope hierarchy does change (which it could do by adding a ng-* directive to a html element held in a custom directive template for example) then the hierarchy can be broken and $parent won't refer to the same scope that it once did.
2) Yes you can. In Chrome developer tools, if you Right Click > Inspect Element (as you have in the screen shot), you select the element. Then if you leave that element highlighted and go to the console tab and type:
angular.element($0).scope()
That returns the scope that the selected element resides in. Then if you type:
angular.element($0).scope().$parent()
That returns the parent scope of the scope that the selected element resides in. And for what it's worth you can also do:
angular.element($0).scope().$parent().$parent()
and keep going up the scope hierarchy.
Also check out the AngularJS Batarang chrome extension which allows you to look through the scopes in the Chrome Developer Tools.
What does this $parent refer to?
Parent refers to the scope of your parent controller. The image in your question doesn't give a clear picture so I will add an example for it.
<div ng-controller="EditorController">
<div ng-controller="ChildEditorController">
....
</div>
</div>
Assume you have a property called editorsList (array) in your EditorController (which is the parent controller here), you can do something like
<div ng-controller="EditorController">
<div ng-controller="ChildEditorController">
Editors Count: {{$parent.editorsList.length}}
</div>
</div>
So you can access the scope of your parent with the $parent.
I am facing a strange situation in which I have created a directive, to which a controller is attached, and one of the two tiny functions of the controller is never called from the view whereas the other function is.
Here is the plunker.
The message I expect is (bold is what does not show up)
You are limited to: Prison
I have already created tens of directives, whether in their own right or as wrappers around existing directives available on GitHub, from lightweight ones such as custom-select to behemoths such as angular-ui-grid.
I am at the end of my wits here as to why {{getArea()}} produces no text at all in the view. I've scrutinized the code, trying to do it with new eyes, so to speak, and I see nothing wrong. I've created a specific project in Eclipse for this tiny piece of code, installed Wampserver just so I could set breakpoints in Firebug and God knows to what great lengths I had to go just so that I could understand what is wrong with the code I wrote.
For instance, in isRestricted(), I can call getArea() without any problem. However, Angular seems to not find the function from the directive.
A few similar questions have already been asked but none of the errors (missing controller or ng-app specification, missing dependency list at module declaration, nested controllers, etc.) seem to apply. There's obviously an important lesson to be learned here and I'm truly eager to learn it.
EDIT: The lesson learned is that ng-if creates a new scope. That new scope comes in between the controller and the directive, which leads to the template of the directive losing access to anything defined in the controller (at least, that's how I would phrase it). (Note that a comment hinted at directive priority.)
There are several solutions, which all maintain the prototypical inheritance needed for the template to access the functions defined in the controller:
not using an isolate scope
not defining the ng-if directive on the top-level element of my directive, as that causes a conflict (between my controller's scope and the scope defined by ng-if). I believe ng-if wins here, which leads to the controller's scope being out of reach of the directive. Using ng-if on a child div does the trick (because then, the ng-if scope inherits my controller's scope, hence making the functions available to the template).
Because of the CSS styling needed with this directive, I have used scope: false.
<span class="scoop-badge-content">{{$parent.getArea()}}</span>
Or in directive :
scope:true
This is because ng-if use how own scope
The strange thing is that when i have this problem i usually use dot notation. But it doesn't work here, probably because we're inside a directive, and i didn't had the case until now.
EDIT : a last way of doing this chaging the template :
<div class="scoop-badge scoop-badge-ua">
<div ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I think this work because you have replace true and ng-if will conflict with ng-scope if it's on the top DOM element.
When you have scope = {} in your directive, Angular creates an isolated scope. Which means it can't get to the getArea() function.
You can completely remove the scope = {} line or set it to scope = true or scope = false depending on what you're trying to achieve later on.
When set to scope = true Angular will create a new scope object and assign it to the directive. This scope object is prototypically inherited from its parent scope.
When set to scope = false the directive will use its parent scope. (This is the default value. It has the same effect if you remove this line).
More information about scopes here
Removing scope: {} from directive definition solves problem.
app.directive('scoopBadgeUa', function() {
return {
restrict : "A",
scope: {}, // This is not needed, creates conflict
templateUrl : "scoop-badge-ua.html",
replace : true,
controller : 'ScoopBadgeUaController',
};
});
Your code is correct, you don't have to do anything more than adding a <div></div> wrapping your code in scoop-badge-ua.html.
<div>
<div class="scoop-badge scoop-badge-ua" ng-class="{'visible': isRestricted()}" ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I like this solution better than transclude: true because it doesn't include the directive's tag in your html code, which ultimately translates to a cleaner code.
angularJS seems to create new scopes, in that there are parent scope, child scope, and sibling scope.
What determines when a new scope is created? For example, if I use ng-inspector to view the scopes present, there is the $rootScope, and also other scopes, but it is not obvious to me what the other scopes correspond to, nor is it clear to me when these other scopes are created/destroyed. I believe they are created/destroyed because the $id changes. Changes occur if I navigate around and press forwards/back.
Angular creates new scope for every instantiated controller on that part of DOM.
Angular also creates scope for every directive (except scope:false, that means for directives with isolated scope, and scope:true). A scope is also created for built in directives such as ng-repeat where it creates scope for every repeated item.
Also when you use ng-if directive it can remove and add parts of the DOM and when it adds it all the controllers and directives will add their scopes again.
Angular's scope tree pretty much will mirror the dom tree. Directives can create new scope. Not just your directives, but also built in Angular directives like ng-if. It's a bit of an in depth topic...
Here is a blog post that explains things more in depth.
Hope that helps!
I have the following example:
http://jsfiddle.net/2FTEU/3/
In the directive I want to assign a dynamic id tag based upon it's position in an ng-repeat
<my-directive id="directive-{{$index}}"></my-directive>
What is happening is that when the directive gets rendered, the index is dropped
<button id="directive-"></button>
How can I use $index in the id for a directive?
EDIT
I need to keep the isolate scope. The example posted is just a small example that demonstrated my issue.
Use {{$parent.$index}}.
The scope created by ng-repeat contains the $index property. Since myDirective creates a new isolate scope, you can use $parent to "reach up" to the parent ng-repeat scope.
Your directive myDirective is using an isolated scope, which does not inherit from the parent scope. Therefore, $index isn't accessible anymore.
You can simply create a new scope for your directive, with {scope : true}. Here is a working version of your Fiddle.
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?
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.
After clicking the first item, the scopes now look like this:
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:
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:
After clicking the first item:
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:
Scopes after clicking on the first item:
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 ..
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:
ng-repeat will create a scope, you really shouldn't be trying to change this.
Priority in directives isn't just execution order - see: AngularJS : How does the HTML compiler arrange the order for compiling?
In Batarang, if you check the performance tab, you can see the expressions bound for each scope, and check if this matches your expectations.