AngularJS re-rendering in ng-repeat inner working - angularjs

The following code contain a input box, a radio button and a button.
When the button is clicked, it will generate one more inputbox and radio button. All the radio button are under the same name.
<ul ng-repeat="i in addQuestion.loop(addQuestion.numOfChoice) track by $index">
<li>
answers:<input type="text"/>
correct:<input type="radio" name="correctChoice" value ={{$index}}/>
</li>
</ul>
<button ng-Click="addQuestion.numOfChoice = addQuestion.numOfChoice + 1">add more choices</button><br/><br/>
//controller:
$scope.addQuestion = {
numOfChoice: 1,
loop: function(num){
return new Array(num);
}
}
My question is while I successfully made this work the way I wanted.
I have no idea how the 'magic' work on the re-rendering whenever a numOfChoice get incremented.
Two questions:
In the ng-repeat it calls the function loop that takes in a parameter: how does the change of the argument trigger a re-render, causing the loop to run again. I would understand if it is a variable.
Whenever I click "add more", it will render one more inputbox and radio button, I don't understand how the states of the previous rendered inputbox/box stay there. In something like reactJS, it will re-render the whole thing, and all the state is lost unless I store it somewhere. How does it store all the state while re-rending the whole ng-repeat loop. Or does it not re-run the ng-repeat but do something else? does it have something to do with the index?
Hope I was clear on my question, please let me know.

The function loop returns a new object which ngRepeat was tracking. So when the value of ng-repeat loop variable changes, it triggers the new rendering.
ngRepeat keeps track of all items in the collection and their corresponding DOM elements. So if the item already exists for example, it will not re-render.

Related

ng-repeater executing every digest cycle

I am trying to understand how angular 1 digest cycles work and how they impact the existing scope. I have 2 controllers one of them is using angular material with a repeater. The second controller is a simple button click event. Both events print to the console to see what is happening.
What i am seeing is that every time i click the button on the second controller the repeater re-runs its entire calculation?
Is this how angular is intended to work? Please see attached the following codepen - when the button is clicked the repeater re-runs on the first controller every single time. I assume this is going to happen every single time any operations occurs on any controller - which just seems like madness.
the repeater code is as follows:
<div flex="50" ng-repeat="item in items">
<md-checkbox ng-checked="exists(item, selected)" ng-click="toggle(item, selected)">
{{ item }} <span ng-if="exists(item, selected)">selected</span>
</md-checkbox>
</div>
At first i thought there was something wrong in my angular but it seems like this happens anywhere full codepen bellow.
Codepen Example
If you don't want ng-repeat to rerun on change then use "track by $index" in ng-repeat
yes this is exactly how it is supposed to work. That is the nature of two-way binding, you constantly check whether one of both values changed.
If you want to turn off that feature and use a one-time binding you can use the :: syntax.
see in the documentation: https://docs.angularjs.org/guide/expression (you need to scroll down to One-time binding. Sadly there are no anchors :D)

NG-Repeat doesn't update cleanly when scope is updated

So I have a list of workspaces:
<li class="workspace-object" ng-repeat="w in workspaces | filter:searchQuery" ng-click="selectWorkspace(w)">
{{w.name | titleCase }}
</li>
That List gets updated in the database by some function, and then I call the loadWorkspaces function below, to re-populate the $scope.workspaces object
$scope.loadWorkspaces = function() {
APIService.getWorkspaces().then(function(data){
$scope.workspaces = data;
});
}
When this happens, the $scope.workspaces, when logged out, reads the right updated information immediately, however, the ng-repeat DUPLICATES, before it updates to the proper list. I have no idea why this happens. Any ideas?
In this example, I am updating the workspace title, and that update runs the loadworkspaces function.
Try this ...
w in workspaces track by $index
When the contents of the collection change, ngRepeat makes the corresponding > changes to the DOM:
When an item is added, a new instance of the template is added to the DOM.
When an item is removed, its template instance is removed from the DOM.
When items are reordered, their respective templates are reordered in the DOM.
By default, ngRepeat does not allow duplicate items in arrays. This is because when there are duplicates, it is not possible to maintain a one-to-one mapping between collection items and DOM elements.
If you do need to repeat duplicate items, you can substitute the default tracking behavior with your own using the track by expression.
For example, you may track items by the index of each item in the collection, using the special scope property $index
Accepted answer did not help me (Using Angular 1.1.10), my repeat was still updating delayed. I struggled for ages before I got it working.
Instead of repeating my custom directive like so;
<my-directive ng-repeat="item in myarray track by item.id" some-binding="item"></my-directive>
I (magically) got it working after moving the ng-repeat to a (non-directive) parent element, like so:
<div ng-repeat="item in myarray track by item.id">
<my-directive some-binding="item"></my-directive>
</div>

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

Dynamic form controls with ng-repeat Angular

In my controller:
$scope.mytest = [1,2,3];
My view:
<div ng-repeat="foo in mytest">
{{foo}}
<input type="text" ng-model="mytest[$index]" name="$index" />
</div>
{{ mytest | json }}
When I load the page it renders this:
I can change the value, and the binding updates all the values:
The only problem, is the form field loses focus. When ng-model updates the scope's values, it causes ng-repeat to manipulate the dom, and the fields lose focus after each key press. I have to click back inside the field between each keypress to enter a value with length more than 1 character.
What I'm trying to accomplish here is to render a text field for each value of an array. Updating the text field should update the scope values, and also update all places I'm displaying those values within the view, both inside & outside of the ng-repeat
Figured it out. I changed my array to be an array of objects with IDs:
$scope.mytest = [{id:1, title:'one'}, {id:2, title:'two'}];
Then I prevented ng-repeat from removing & re-inserting a dom node when it changes:
<div ng-repeat="foo in mytest track by foo.id">
My previous attempt from the original post fails. In this example illustrating why, you can see that Angular can't discern what changes were made:
[1,2,3] // point A
[2,2,3] // point B
Angular has no way of knowing what took place to get from point A to B. Did I edit the first value? Remove it & and insert a new value? Make multiple edits that cancel each other out?
In this example, Angular is able to discern what happened to get from Point A to B:
$scope.mytest = [{id:1, title:'one'}, {id:2, title:'two'}]; // point A
$scope.mytest = [{id:1, title:'two'}, {id:2, title:'two'}]; // point B
In this example I use the "value object" design pattern. I wrap my scalar values in an object which has an identifier that is immutable, while the wrapped value remains mutable.
Combined with the "track by" syntax of ng-repeat, it allows Angular to discern between an add & subsequent removal versus an edit. ng-repeat can then directly update the bindings in the isolate scope, as opposed to redrawing the whole darn page.

elasticui - Triggering sort with ng-click only firing on first click

I'm trying to make the sort functionality work by creating two buttons: relevance and date.
I've set it up the following way:
<div ng-init="sort=false">
<a ng-click="sort=true">Date</a>
<a ng-click="sort=false">Relevance</a>
then on the container with the results:
<div eui-sort="ejs.Sort('post_date').order('desc')" eui-enabled="sort" >
The value set with ng-init properly affects the initial sort order and when I click date the list sorts as intended, but when I click relevance the list does not re-sort back as if eui-enabled was set to false.
I'm guessing underformed knowledge of Angular is causing me to oversimplify this. Any advice?
I suspect you're running into the AngularJS dot-problem, i.e.: the sort. A way to circumvent this is modifying sorting.sort within the eui-sort scope:
<div eui-sort="ejs.Sort('post_date').order('desc')" eui-enabled="true">
<a ng-click="sorting.enabled=true">Date</a>
<a ng-click="sorting.enabled=false">Relevance</a>
</div>
In this example, eui-enabled is only used for initialization since the value (true) doesn't change. Note that to reference the "sorting: object you must be inside the scope of the eui-sort (i.e.: inside the div)

Resources