In angularjs 1.2 operations like filtering an ng-repeat
with many rows (>2,000 rows) can become quite slow (>1 sec).
I know I can optimize execution times u
What you can do is detect the end of the ngRepeat
as this post says.
I'll do something like, in the controller:
$scope.READY = false;
And in the directive, as the post above says, I'll do something like:
if (scope.$last) {
$scope.READY = true;
}
And you can have a css based loader/spinner with
<div class="loader" ng-show="!READY">
</div>
Ofcourse you can also have css based animations which are independent of js execution.
Use spin.js and the site http://fgnass.github.com/spin.js/ shows the step which is quite easy. the loading animation is in CSS which separated from the UI thread and therefore loaded smoothly.
The problem is that as long as Javascript is executing, the UI gets no chance to update. Even if you present a spinner before filtering, it will appear "frozen" as long as Angular is busy.
A way to overcome this is to filter in chunks and, if more data are available, filter again after a small $timeout
. The timeout gives the UI thread a chance to run and display changes and animations.
A fiddle demonstrating the principle is here.
It does not use Angular's filters (they are synchronous). Instead it filters the data
array with the following function:
function filter() {
var i=0, filtered = [];
innerFilter();
function innerFilter() {
var counter;
for( counter=0; i < $scope.data.length && counter < 5; counter++, i++ ) {
/////////////////////////////////////////////////////////
// REAL FILTER LOGIC; BETTER SPLIT TO ANOTHER FUNCTION //
if( $scope.data[i].indexOf($scope.filter) >= 0 ) {
filtered.push($scope.data[i]);
}
/////////////////////////////////////////////////////////
}
if( i === $scope.data.length ) {
$scope.filteredData = filtered;
$scope.filtering = false;
}
else {
$timeout(innerFilter, 10);
}
}
}
It requires 2 support variables: $scope.filtering
is true
when the filter is active, giving us the chance to display the spinner and disable the input; $scope.filteredData
receives the result of the filter.
There are 3 hidden parameters:
counter < 5
) is small on purpose to demonstrate the effect$timeout(innerFilter, 10)
) should be small, but enough to give the UI thread some time to be responsiveThis is only a proof of concept; I would suggest refactoring it (to a directive probably) for real use.
Promise/deferred can be used in this case, you can call notify to watch the progress of your code, documentation from angular.js: https://code.angularjs.org/1.2.16/docs/api/ng/service/$q Here is a tutorial on heavy JS processing that uses ng-Promise: http://liamkaufman.com/blog/2013/09/09/using-angularjs-promises/, hope it is helpful.
//app Factory for holding the data model
app.factory('postFactory', function ($http, $q, $timeout){
var factory = {
posts : false,
getPosts : function(){
var deferred = $q.defer();
//avoiding the http.get request each time
//we call the getPosts function
if (factory.posts !== false){
deferred.resolve(factory.posts);
}else{
$http.get('posts.json')
.success(function(data, status){
factory.posts = data
//to show the loading !
$timeout(function(){
deferred.resolve(factory.posts)
}, 2000);
})
.error(function(data, status){
deferred.error('Cant get the posts !')
})
};
return deferred.promise;
},
getPost : function(id){
//promise
var deferred = $q.defer();
var post = {};
var posts = factory.getPosts().then(function(posts){
post = factory.posts[id];
//send the post if promise kept
deferred.resolve(post);
}, function(msg){
deferred.reject(msg);
})
return deferred.promise;
},
};
return factory;
});
Here are the steps:
ng-view
with
fixed
positioning.<div class="activity-box" ng-show="!!message">
<img src="img/load.png" width="40" height="40" />
<span>{{ message }}</span>
</div>
activity
Directive:A simple directive with a single attribute message
. Note the ng-show
directive in the template above. The message
is used both to toggle the activity indicator and also to set the info text for the user.
app.directive('activity', [
function () {
return {
restrict: 'EA',
templateUrl: '/templates/activity.html',
replace: true,
scope: {
message: '@'
},
link: function (scope, element, attrs) {}
};
}
]);
<body ng-controller="appController">
<div ng-view id="content-view">...</div>
<div activity message="{{ activityMessage }}"></div>
</body>
Note that the activity
directive placed outside of ng-view
. It will be available on each section of your single-page-app.
app.controller('appController',
function ($scope, $timeout) {
// You would better place these two methods below, inside a
// service or factory; so you can inject that service anywhere
// within the app and toggle the activity indicator on/off where needed
$scope.showActivity = function (msg) {
$timeout(function () {
$scope.activityMessage = msg;
});
};
$scope.hideActivity = function () {
$timeout(function () {
$scope.activityMessage = '';
}, 1000); // message will be visible at least 1 sec
};
// So here is how we do it:
// "Before" the process, we set the message and the activity indicator is shown
$scope.showActivity('Loading items...');
var i;
for (i = 0; i < 10000; i += 1) {
// here goes some heavy process
}
// "After" the process completes, we hide the activity indicator.
$scope.hideActivity();
});
Of course, you can use this in other places too. e.g. you can call $scope.hideActivity();
when a promise resolves. Or toggling the activity on request
and response
of the httpInterceptor
is a good idea too.
.activity-box {
display: block;
position: fixed; /* fixed position so it doesn't scroll */
z-index: 9999; /* on top of everything else */
width: 250px;
margin-left: -125px; /* horizontally centered */
left: 50%;
top: 10px; /* displayed on the top of the page, just like Gmail's yellow info-box */
height: 40px;
padding: 10px;
background-color: #f3e9b5;
border-radius: 4px;
}
/* styles for the activity text */
.activity-box span {
display: block;
position: relative;
margin-left: 60px;
margin-top: 10px;
font-family: arial;
font-size: 15px;
}
/* animating a static image */
.activity-box img {
display: block;
position: absolute;
width: 40px;
height: 40px;
/* Below is the key for the rotating animation */
-webkit-animation: spin 1s infinite linear;
-moz-animation: spin 1s infinite linear;
-o-animation: spin 1s infinite linear;
animation: spin 1s infinite linear;
}
/* keyframe animation defined for various browsers */
@-moz-keyframes spin {
0% { -moz-transform: rotate(0deg); }
100% { -moz-transform: rotate(359deg); }
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(359deg); }
}
@-o-keyframes spin {
0% { -o-transform: rotate(0deg); }
100% { -o-transform: rotate(359deg); }
}
@-ms-keyframes spin {
0% { -ms-transform: rotate(0deg); }
100% { -ms-transform: rotate(359deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
Hope this helps.
Here is an working example :-
angular
.module("APP", [])
.controller("myCtrl", function ($scope, $timeout) {
var mc = this
mc.loading = true
mc.listRendered = []
mc.listByCategory = []
mc.categories = ["law", "science", "chemistry", "physics"]
mc.filterByCategory = function (category) {
mc.loading = true
// This timeout will start on the next event loop so
// filterByCategory function will exit just triggering
// the show of Loading... status
// When the function inside timeout is called, it will
// filter and set the model values and when finished call
// an inbuilt $digest at the end.
$timeout(function () {
mc.listByCategory = mc.listFetched.filter(function (ele) {
return ele.category === category
})
mc.listRendered = mc.listByCategory
$scope.$emit("loaded")
}, 50)
}
// This timeout is replicating the data fetch from a server
$timeout(function () {
mc.listFetched = makeList()
mc.listRendered = mc.listFetched
mc.loading = false
}, 50)
$scope.$on("loaded", function () { mc.loading = false })
})
function makeList() {
var list = [
{name: "book1", category: "law"},
{name: "book2", category: "science"},
{name: "book1", category: "chemistry"},
{name: "book1", category: "physics"}
]
var bigList = []
for (var i = 0; i <= 5000; i++) {
bigList = bigList.concat(list)
}
return bigList
}
button {
display: inline-block;
}
<html>
<head>
<title>This is an Angular Js Filter Workaround!!</title>
</head>
<body ng-app="APP">
<div ng-controller="myCtrl as mc">
<div class = "buttons">
<label>Select Category:- </label>
<button ng-repeat="category in mc.categories" ng-click="mc.filterByCategory(category)">{{category}}</button>
</div>
<h1 ng-if="mc.loading">Loading...</h1>
<ul ng-if="!mc.loading">
<li ng-repeat="ele in mc.listRendered track by $index">{{ele.name}} - {{ele.category}}</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
</body>
<html>