ng-repeat view "lagging" behind the actual model? - angularjs

HTML:
{{vm.regions}}
<div ng-repeat="region in vm.regions">
{{region}}
</div>
On button presses, the model a.k.a vm.regions gets updated in the controller. For example vm.regions = [].
I can see that the array {{vm.regions}} is instantly updated, but the elements in the div take at least a second to update, meaning you can see old elements for a bit in the newly updated list, for example.
What is causing this?

It is in AngularJs Best Practices to always add "track by $index" in order to inprove performances.
{{vm.regions}}
<div ng-repeat="region in vm.regions track by $index">
{{region}}
</div>
Link : https://docs.angularjs.org/api/ng/directive/ngRepeat

Try using vs-repeat. It applies virtual scrolling to ng-repeat which increases its performance drastically even if you are not using one time binding.
http://kamilkp.github.io/angular-vs-repeat/#?tab=8

Related

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.

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)

quick-ng-repeat does not update view

I use quick-ng-repeat Quick-ng-repeat for my list to iterate because it is a huge list and I would improve performance.
Now I recognized that if I change the model, the view is not updated with this code:
<span quick-ng-repeat="item in collection track by $index" quick-repeat-list="items">
{{ item }}
</span>
With this code
<span ng-repeat="item in collection track by $index">
{{ item }}
</span>
every works fine.
Does anyone have any idee why this happens?
Thanks a lot!
quick-ng-repeat does not implement deep watch just like ng-repeat, it implement one way binding approach. So if your model changes frequently, don't use it.
In Angular 1.2 a new addition was made to the syntax of ngRepeat: the amazingly awesome track by clause. It allows you to specify your own key for ngRepeat to identify objects by, instead of just generating unique IDs.
This means that you can change the above to be ng-repeat="task in tasks track by task.id" and since the ID would be the same in both your original tasks and the updated ones from the server – ngRepeat will know not to recreate the DOM elements and reuse them
The Quick ng-repeat is doing one way binding ,it something similar to what we do in angularjs
<div ng-repeat="item in ::items">{{item.name}}</div>
This does not create unnecessary watches.
I think Quick-ng-repeat use Single binding same as in angular 1.4 like (::).Single binding mean it will be create a watcher for your repeat that why it will not effect when object is change.
<div ng-repeat="obj in ::objList">{{obj.name}}</div>

Using same ng-repeat on one page - best practice?

In need the data from a ng-repeat on two places within the view. The code below works, but I am not sure if this is the way to do it. (I am new to Angular).
If a user clicks on a outlet-option the outlet-products get displayed. First I loaded the products below the outlet-option and used jQuery to move to the productsWrapper if a user clicks on an option. But then I needed to compile it again which made it a bit messy in my opinion.
There can be only one "#productsWrapper" and one ".outlet", so I came up with using the ng-repeat twice. But is this the way to go?
<div id="productsWrapper">
<div ng-repeat="element in elements track by $index" class="products" style="display: none;" id="prod-{{element.wonelement_id}}">
<outlet-product ng-repeat = "option in element.options"
class = "product"
option-data = "option"
element-data= "element"
chosen-data = "chosen">
</outlet-product>
</div>
</div>
<div class="outlet">
<div ng-repeat="element in elements track by $index">
<outlet-option
element-data = "element"
option-data = "element.option"
chosen-data = "chosen"
app-settings = "app">
</outlet-option>
</div>
</div>
I think what you proposed looks fine in a typical AngularJS way.
If you were applying filters to the ng-repeat clause, then I would have suggested applying that filter ahead of time in the controller so that it would only be run the one time. But since you are using no filters at all on the data, I think it is fine as you have it.

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

Resources