Prevent AngularJs content flash on $scope update - angularjs

When I update my $scope like so in a controller
$scope.item = "Hello";
Then the whole DOM for item seems to be removed and then added again. This seems fine, but if I have a list of items and do
$scope.items = Resource.query();
To update all the items then all of the DOM for items is removed and then re-added, this looks a broken and clumsy - is there anyway around this removing and then adding of the DOM elements when $scope is updated?
This issue is further exasperated if $scope.items and its children are used inside several ng-repeat statements as all of those ng-repeat sections are removed and then re-added.
EDIT
I have read this and feel that this is the issue https://www.binpress.com/tutorial/speeding-up-angular-js-with-simple-optimizations/135
That I have so much "stuff" going on the $digest is just slow. I am working on an example but in the mean time imagine this try of data
{
{
id: 1,
name: "name1",
something: {
id: 10,
name: "something10"
else: {
id: 15,
name: "else15"
}
}
}
}
But there are 20 such objects all with nested objects - this appears to be the issue. That there are so many objects being parsed and bound to the DOM that the $watchers are just taking a long time to go over everything.
EDIT 2
I made this demo, perhaps I am using resource wrong? http://plnkr.co/edit/QOickL0Dyi8jmuvG9mzN
But the items are replaced every 5 seconds, on replace they all disappear and then reappear. This is the issue I am having.

This is a common problem when using ng-repeat. In order to track the objects the directive itself by default adds additional property to every object in the array ($$hashkey). And whenever new object is added or removed from the array the same is happening for it's associating DOM element. When the array is replaced with new array (ex. returned from the server) as you mentioned all the DOM elements representing objects from the previous array are removed and replaced with new DOM elements for the new objects. This is simply because the new objects in the array does not have the $$hashkey property even though they may be semantically the same.
A solution for this common problem came in Angular 1.2 version with the track by clause. You can find more detailed explanation about it in this post.
In your case the objects in the array have an 'id' property which it's a good candidate for tracking them if you can guarantee it's uniqueness. So using ng-repeat like this will improve it's performance.
<div ng-repeat="item in items track by item.id">
<!-- tpl -->
</div>

If you'll take a look at $resource documentation, you'll see that the correct way of manipulation with data - to use callback function
Instead of
$scope.items = Resource.query();
Try
Resource.query(function(data) {
$scope.items = data;
});

Did you try ngCloak directive? https://docs.angularjs.org/api/ng/directive/ngCloak

Related

Binding nested array in Angular view

I am having difficulty binding nested array using ng-Repeat. Is there a way I can bind complex array object to view.
Scope in directive holds object.
(directive controller is as - controllerAs: 'srCtrl')
var vm = this;
vm.resultCategories = [[{"id":3,"name":"name3","desc":"2","categoryId":1,"categoryName":"cat 1"},
{"id":1,"name":"name1","desc":"2","categoryId":1,"categoryName":"cat 1"},
{"id":2,"name":"name2","desc":"2","categoryId":1,"categoryName":"cat 1"}],
[{"id":4,"name":"name4","desc":"name1","categoryId":2,"categoryName":"cat 2"},
{"id":2,"name":"name2","desc":"2","categoryId":2,"categoryName":"cat 2"}]];
Binding directive view
<ul>
<li ng-repeat="category in srCtrl.resultCategories">
<h3>{{category.categoryName}}</h3>
</li>
</ul>
Any help is appreciated. Many thanks in advance.
The best solution is to concat all the arrays using a reduce function and a concat function before you serve it up to a ui. You can either do this through a filter or just within the controller upon receiving the data:
vm.resultCategories = vm.resultCategories.reduce(function(last,next){
return last.concat(next);
})
Note there are ways to add nested ng-repeat, but this is not advised for a variety of reasons:
you are using list items, it would mean you need to create divs to put them together (this could affect your css and overall structure)
confusing as hell!
room for potential error
edit:
Another way you achieve this(with less lines) is to use:
vm.resultCategories = new Array(vm.resultCategories.join());
Note: Although this method is shorter, it actually takes longer to run since the cpu is creating a 'new' array each time.

ngRepeat seems to use a random order instead of order given

I have a json containing objects originating from my Symfony controller.
I need to loop over the objects like so
<li data-ng-repeat="course in courses"></li>
In my app controller I do this
$scope.courses = {{ courses | serialize('json', serialization_context().setGroups(['identification', 'courseListing', 'portalOverview'])) | raw }};
The order is okay when I check the scope variable 'courses' with the ng-inspect browser plugin.
Screenshot : http://i.imgur.com/pKy00uo.png
But still the loop seems kinda random. The last object keeps getting placed after the 2nd child.
Any ideas? Need more info, just ask. Thanks!
Okay, so it seems Angular doesn't handle objects too well when given to ng-repeat.
I transformed my object of objects to an array of objects and the objects were rendered in the correct order.
var arrCourses = [];
$.each($scope.courses, function(index, obj){
arrCourses.push(obj);
});
$scope.courses = arrCourses;

Why does it improve ng-repeat performance?

Let's suppose a function called refreshList aiming to call a REST API to retrieve a JSON array containing items.
$scope.refreshList = function () {
// assuming getting the response object from REST API (Json) here
$scope.items = response.data.listItems;
}
And a basic ng-repeat on the HTML side:
<div ng-repeat="item in items track by item.id">
Then, I added a onPullToRefresh function, triggered by this directive, aiming to trigger a refresh of the whole list.
$scope.onPullToRefresh = function () {
$scope.items = [];
$scope.refreshList();
}
It's HTML is:
<ion-refresher on-refresh="onPullToRefresh()">
</ion-refresher>
The issue is that some pull-to-refresh (while scrolling to the top) can crash the app randomly...
Thus, I decided to alter the $scope.onPullToRefresh function like this:
$scope.onPullToRefresh = function () {
//$scope.items = []; commenting this line, causing the potential memory leak and the crash of the app
$scope.refreshList();
}
With this update, the app never crashes any more on my iOS device :)
However, I don't figure out what is the main difference by keeping the line emptying the array.
Does it impact directly the ng-repeat directive, causing some mess since the REST API's promise would reassign the content immediatly?
Indeed, Xcode IDE (since cordova app) indicated this error before applying this fix: EXC_BAD_ACCESS.
What might be the reason of this behavior?
I'm pointing out that Ionic (the framework I'm using) is fully tested with Angular 1.2.17 currently, so I'm using this version.
This probably isn't the greatest answer as I'm not 100% certain, but I would suggest it has something to do with the digest.
The ng-repeat likes to work with the one array and not be cleaned out between refreshes. It's designed more to allow you to dynamically add/remove items from the array and the view is updated accordingly in the digest loop. You can do some nice animations for items being added and taken away etc with this.
So when the digest arrives, you've scrapped the array it was watching and thrown a new array into the memory it had.
Try, instead of clearing the array $scope.items = [];, to .pop() each item in the array while it has length.
it will have very little impact to do it this way. Otherwise, the best "Angular" way for the ng-repeat is to do it by just adding the new items to the array and remove any that are now gone.

AngularJS: Deleting from scope without hardociding

I have an array of items bound to <li> elements in a <ul> with AngularJS. I want to be able to click "remove item" next to each of them and have the item removed.
This answer on StackOverflow allows us to do exactly that, but because the name of the array which the elements are being deleted from is hardcoded it is not usable across lists.
You can see an example here on JSfiddle set up, if you try clicking "remove" next to a Game, then the student is removed, not the game.
Passing this back from the button gives me access to the Angular $scope at that point, but I don't know how to cleanly remove that item from the parent array.
I could have the button defined with ng-click="remove('games',this)" and have the function look like this:
$scope.remove = function (arrayName, scope) {
scope.$parent[arrayName].splice(scope.$index,1);
}
(Like this JSFiddle) but naming the parent array while I'm inside it seems like a very good way to break functionality when I edit my code in a year.
Any ideas?
I did not get why you were trying to pass this .. You almost never need to deal with this in angular. ( And I think that is one of its strengths! ).
Here is a fiddle that solves the problem in a slightly different way.
http://jsfiddle.net/WJ226/5/
The controller is now simplified to
function VariousThingsCtrl($scope) {
$scope.students = students;
$scope.games = games;
$scope.remove = function (arrayName,$index) {
$scope[arrayName].splice($index,1);
}
}
Instead of passing the whole scope, why not just pass the $index ? Since you are already in the scope where the arrays are located, it should be pretty easy from then.

Linking MVC In AngularJS

I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).

Resources