Why does the ng-repeat directive create a child scope? - angularjs

Question: Why does the ng-repeat directive create a child scope?
I don't understand why this is the case. Creating a new scope makes sense to an extent if you explicitly create a child controller, but why would it automatically create one for every ng-repeat?
I guess the reason it confuses me is because if you create a loop in JS, that doesn't mean the code outside the loop can't access any of the variables inside of it.
Example:
for(var x=0; x<10; x++) {
var y = x
}
alert(y);

From the docs (with my emphasis):
The ngRepeat directive instantiates a template once per item from a
collection. Each template instance gets its own scope, where the given
loop variable is set to the current collection item, and $index is set
to the item index or key.
Seems pretty straightforward, doesn't it?

The DOM is not javascript. The items in the collection you are ng-repeating over need to be bound to separate scopes that are bound to the repeated DOM elements and creates a tree of scopes that mirror structure of the page (somewhat)
ex, this markup:
<body ng-app="myapp">
<div ng-repeat="item in items">
<p>{{ item.name }}</p>
</div>
</body>
will produce something like the DOM on the left and scope on the right:
body rootScope
div scope
p
div scope
p
(somewhat approximate for illustration)

Javascript was written by a single person in 10 days. In almost every other language, loops create a separate scope and your code example wouldn't compile or you'd end up with null/none for y in your last line.

Related

ng-repeat one-time binding inheritance?

If I have an ng-repeat directive iterating through a list of lists via a one-time binding, does a nested ng-repeat through each of the inner lists also need a one-time binding, or is that redundant?
<div ng-repeat="list in ::$ctrl.lists">
<span ng-repeat="item in list">{{item}}</span>
</div>
Do I need to change the inner ng-repeat to item in ::list? $ctrl.lists will never change in any manner.
It is specify nowhere that is is inherit, event if you have bind the array, object inside the array can still change, so it is better to put '::' for the span loop too.
http://jsfiddle.net/vw2fjxys/64/
var a = [1,2,3]
setTimeout(function(){
console.log(2)
a.length = 2
$scope.$apply();
},3000)
$scope.lists = [a];
You can see on the exemple after 2 second that with :: it is not recalculated for the inner loop but without it is.

Filter a directive by using a div wrapper or within the directive tag

I'm trying to go with the best approach and avoid unnecessary rendering/processing time in my AngularJS app when choosing between 2 directives to be displayed in the page inside an ngRepeat loop, want to know which is the best way:
If by setting the ng-if directly in the directive html element, like:
<div ng-repeat="element in list">
<my-directive-a ng-if="someFunction(element)"></my-directive-a>
<my-directive-b ng-if="!someFunction(element)"></my-directive-b>
</div>
Or by moving out the first <div> from the directive's template and use it as a wrapper for each directive. For instance:
<div ng-repeat="element in list">
<div ng-if="someFunction(element)">
<my-directive-a></my-directive-a>
</div>
<div ng-if="!someFunction(element)">
<my-directive-b></my-directive-b>
</div>
</div>
NOTE: The starting <div> element on each directive could be modified behave the same so I will basically take that out of the directive's html and moving it outside the directive declaration in order to place the ng-if there
What would be the best approach for this case? Are there any performance implications from doing it one way or another? Or is it just the same thing? Consider that the number of elements in the list could get really big.
They are quite the same, but you can improve performance with one-time binding, but only when element does not change at runtime (for example, let's say that it has property name, and your someFunction is like return element.name === 'John'). Angular just stop observing this function when it returns value, and watches will be deleted. There are 2 prerequisites to use this solution:
Elements properties in list does not change (if you rely on them in someFunction), for example if you rely on name property name must not change, because watcher on someFunction is note available.
When list changes or its elements properties change, you reload all list (for example, you fetch it from server again if you know that change occurred)
What you get with this? There is no watches after my-directives are drawn on ng-ifs, and when something changes, new reference is bound to list (for example, it comes from server) and everything will be redrawn, ng-ifs will run again and when will become stable (function returns value) then will be unbound. How it looks like? Like this:
<div ng-repeat="element in list">
<div ng-if="::(someFunction(element))">
<my-directive-a></my-directive-a>
</div>
<div ng-if="::(!someFunction(element))">
<my-directive-b></my-directive-b>
</div>
</div>
Two colons before expression. But be aware, that with one-time binding it's easy to mess up - you need to be sure that you test your code enough to be sure it works.

Using a scope variable to index a scope array in ng-repeat?

I have a tree like structure, received as JSON from some PHP code which I wrote for my server.
The code is far to complex to post, so let's use an example:
A company has
multiple departments, each of which has 0..n
jobs, each of which has 0..n
people
So, I can find myself with something like
$scope.departments[1].jobs[1].people[1]
$scope.departments[1].jobs[1].people[2]
etc
I have $scope variable for the current user-selected department, job and person.
My problem is where I want to use an ng-repeat of the jobs in the HTML view for the jobs.
The statement
<div ng-repeat="job in departments[{{departmentId}}].jobs>
gives Error: [$parse:syntax], as does
<div ng-repeat="job in departments[$scope.departmentId].jobs>
(which I tried in desperation).
What is the correct syntax?
I am wondering if I will need to try
<div ng-repeat="job in GetJobsForCurrentDepartment()>
since $scope.departmentId would be in scope in my controller, but is not in the HTML view for the departments.
Since a view has a controller associated with it, and controller has a $scope associated with it. It is not required to use $scope the html element for referencing the variables defined in the controller. Any variable used with a element in a view is looked up in the scope associated with that element. $scope is required only in the script.
Thus you can write
<div ng-repeat="job in departments[departmentId].jobs>
This will fetch the value of departmentId & jobs inside it will be processed by the ng-repeat directive.
You don't have to mention $scope attached with a varable in view
try like this
<div ng-repeat="job in departments[departmentId].jobs>
JS
$scope.departmentId=1;

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

Difference between onLoad and ng-init in angular

I am learning angular. I don't understand what is difference between onLoad and ng-init for initialization of a variable. In which scope it creates this variable.
For example
<ng-include onLoad="selectedReq=reqSelected" src="'partials/abc.html'"></ng-include>
OR
<ng-include ng-init="selectedReq=reqSelected" src="partials/abc.html"></ng-include>
Please also give me some idea about isolated scope.
ng-init is a directive that can be placed inside div's, span's, whatever, whereas onload is an attribute specific to the ng-include directive that functions as an ng-init. To see what I mean try something like:
<span onload="a = 1">{{ a }}</span>
<span ng-init="b = 2">{{ b }}</span>
You'll see that only the second one shows up.
An isolated scope is a scope which does not prototypically inherit from its parent scope. In laymen's terms if you have a widget that doesn't need to read and write to the parent scope arbitrarily then you use an isolate scope on the widget so that the widget and widget container can freely use their scopes without overriding each other's properties.
From angular's documentation,
ng-init SHOULD NOT be used for any initialization. It should be used only for aliasing.
https://docs.angularjs.org/api/ng/directive/ngInit
onload should be used if any expression needs to be evaluated after a partial view is loaded (by ng-include).
https://docs.angularjs.org/api/ng/directive/ngInclude
The major difference between them is when used with ng-include.
<div ng-include="partialViewUrl" onload="myFunction()"></div>
In this case, myFunction is called everytime the partial view is loaded.
<div ng-include="partialViewUrl" ng-init="myFunction()"></div>
Whereas, in this case, myFunction is called only once when the parent view is loaded.
Works for me.
<div ng-show="$scope.showme === true">Hello World</div>
<div ng-repeat="a in $scope.bigdata" ng-init="$scope.showme = true">{{ a.title }}</div>

Resources