How to show a busy message while angular digest is running - angularjs

I have a complex page (lots of ng-repeats nested) so the digest takes a while to finish. I want to give the user some feedback, so they don't think the browser is hung.
Below is a sample fiddle, when you click HIT ME the $watch hangs for 2 seconds. I want the "Working" message to show up, but it does not.
https://jsfiddle.net/jdhenckel/c7edvdt1/
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.n = 0;
$scope.test = function() {
$scope.msg = 'Working...';
$scope.n += 1;
};
$scope.$watch(function(scope) {
return $scope.n;
}, function() {
var x = Date.now() + 2000;
while (x > Date.now()) {}
$scope.msg = 'Done.';
});
});
I also tried to use JQuery to directly change the DOM before the digest, but that also didn't work. Seems like my only option is to move all the long running stuff a future digest using a $timeout, but that seems like a hack!
Is there an elegant way to notify the user that the digest is running?
EDIT: Here is a possibly more realistic example.
https://jsfiddle.net/jdhenckel/9vcLq0k3/
$scope.n = 0;
$scope.msg = 'Ready';
$scope.test = function() {
$scope.msg = 'Working...';
$timeout(function() {
doStuff();
$scope.msg = 'Done';
}, 100);
}
This works because I moved all the expensive changes into doStuff.
I was hoping that Angular would provide a simpler way to do this (such as ng-cloak for initialization.) If not, then I'll keep using $timeout.

Related

Measure page load of angular app

Hi All I have tried the var loadTime = window.performance.timing.domContentLoadedEventEnd- window.performance.timing.navigationStart; but value seems to be much smaller than actual time it takes to load the page.
Ideally I want to have a stopwatch that stop counting when the page is fully loaded. Any suggestion?
I won't say that this method is not without its flaws, but if you are measuring the loading of data, here is one method you can use to achieve a loading counter.
Create a bool to evaluate against, isLoading. Hoist it to true.
Create your int, loadTime.
Use a SetInterval to increment the int
Load some data
after you have processed the data, set the isLoading condition to false.
Example:
https://plnkr.co/edit/ARVpFd9NmlFNURlyS9SZ
app.controller("myCtrl", function($scope, $http) {
$scope.isLoading = true;
$scope.loadTime = 0;
$scope.msg = '';
var tmr = setInterval(function() {
if(!$scope.isLoading || $scope.isLoading === false) {
clearInterval(tmr);
}
$scope.loadTime += 0.01;
}, 100);
$http.get('https://unsplash.com')
.then(function(res) {
$scope.msg = 'Data is loaded';
$scope.isLoading = false;
});

Intentional delaying in Angularjs

I am making a little diagnose web page and want to make it look like the system is working hard in background by showing loading image for a while.
I am not a programmer. so I basically look all over for a chunk of codes that works and this is how I tried... but justDelaying function never delays the later process. It seems all the process is running simultaneously.
app.controller('HardworkCtrl', function($scope, $timeout) {
$scope.hardWork = function() {
// start showing hard-working image
$scope.loading = true;
$scope.justDelaying = function() {
$timeout(function() {
// do nothing...
}, 5000);
}
$scope.justDelaying();
$scope.theAnswer = "42."
// stop showing hard-working image
$scope.loading = false;
};
};
Any idea?
Anything that happens inside "$timeout" is asynchronous, so it's just scheduled for later, without blocking the main execution. In other words, the instructions after you call $scope.justDelaying() happen immediately, while the instructions inside justDelaying get delayed for 5 seconds. To make those instructions execute later, you need to move them inside the $timeout, like this:
app.controller('HardworkCtrl', function($scope, $timeout) {
$scope.hardWork = function() {
// start showing hard-working image
$scope.loading = true;
$scope.delayThenDoStuff = function() {
$timeout(function() {
$scope.theAnswer = "42."
// stop showing hard-working image
$scope.loading = false;
}, 5000);
}
$scope.delayThenDoStuff();
};
};

Testing a sequence of async updates with Protractor?

Let's say there's a button that triggers some sort of data processing which takes a while. During processing, I want to tell the user where we are by displaying the messages like "Working...", "Working very hard...", "Almost there...", etc. It is guaranteed that all those messages will appear one after another before the processing is completed. The task is to check this scenario with Protractor.
So, here's an example code:
<div ng-controller="AppController">
<p>{{message}}</p>
<button type="button" ng-click="go()">Go!</button>
</div>
...
<script>
angular.module('app', [])
.controller("AppController", function($scope, $timeout) {
$scope.message = "";
$scope.go = function() {
$scope.message = "Working...";
$timeout(function() {
$scope.message = "Working very hard...";
$timeout(function() {
$scope.message = "Almost there...";
$timeout(function() {
$scope.message = "Done!!!";
}, 1000);
}, 1000);
}, 1000);
};
});
</script>
I understand this behavior is relatively easy to test with regular unit tests (Jasmine), yet let's pretend these $timeout calls are actually async updates sent by backend via a websocket.
Is it possible to somehow test this sequence of updates with Protractor?
The straightforward approach like this:
expect(element(by.binding('message')).getText()).toEqual('Working...');
expect(element(by.binding('message')).getText()).toEqual('Working very hard...');
doesn't work. In my case the test fails with:
Expected 'Working very hard...' to equal 'Working...'.
which is understandable, I assume that Protractor just waits for all pending things to finish before moving on.
One approach I can think of is, explicitly polling the DOM to monitor when element content gets changed. This I would like to avoid. Are there any better options available?
You can give a try to the new feature: expected conditions (was added 10 hours ago):
var EC = protractor.ExpectedConditions;
var message = element(by.binding('message');
browser.wait(EC.textToBePresentInElement(message, 'Working...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Working very hard...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Almost there...'), 1000);
browser.wait(EC.textToBePresentInElement(message, 'Done!!!'), 1000);

Can I use $interval in a service?

Short version: please reply to the title. Thanks for your help.
Longer version: I started out, as I suppose many n00bs do, with a little code, added some, bit by bit, and found myself with everything in one huge controller.
So, I split my functionality and had a bunch of smaller controllers.
Then I wanted them to communicate with each other & I discovered services.
Then I read that controllers should be lean & mean and I started to move lots of logic from controllers to services.
Now I find that some old code, which read
$scope.internetConnectionRetryTimer = $interval($scope.attemptInternetConnection, RECONNECT_ATTEMPT_FREQUENCY);
when moved into a service as
this.internetConnectionRetryTimer = $interval(this.attemptInternetConnection, RECONNECT_ATTEMPT_FREQUENCY);
doesn't seem to be running the timer; either that or it is not calling the function upon expiry.
Same question as the short version: can I actually use $interval in a service?
[Update] here's the code:
global vars SERVER is a URL and var RECONNECT_ATTEMPT_FREQUENCY = 5 * 1000; // 5 seconds
this.attemptInternetConnection = function()
{
$interval.cancel(this.internetConnectionRetryTimer);
var params = '?action=test_connection&user=dummy';
$http.get(SERVER + params).
success(function()
{
$interval.cancel(this.internetConnectionRetryTimer);
$rootScope.$broadcast('internetIsAvailable');
})
.error(function(status)
{
this.internetConnectionRetryTimer = $interval(this.attemptInternetConnection, RECONNECT_ATTEMPT_FREQUENCY);
$rootScope.$broadcast('internetIsUnavailable');
});
};// attemptInternetConnection()
No problem with that.
Here's an example:
<div ng-app="myApp" ng-controller="myCtrl">{{Data.Test}}</div>
angular.module('myApp', []).
controller('myCtrl', function ($scope, myService) {
$scope.Data = {Test: 'Test'};
myService.ChangeTest($scope.Data);
}).
service('myService', function ($interval) {
this.ChangeTest = function (data) {
$interval(function () {
if (data.Test == 'Test') data.Test = 'Changed Test';
else data.Test = 'Test';
},500);
}
});
Here's a Fiddle.
That should work fine. Though it depends how the method attemptInternetConnection has been written as the code is not posted. If you are referrencing any variables specific to the service inside attemptInternetConnection, it should be accessed by a referrence to the service object like the sample given below.
Demo: http://plnkr.co/edit/1J0qzw044WRHSFGvZyOD?p=preview
app.service('intervalTest', function($interval) {
var me = this;
me.comments = [{
total: 3,
comment: 'some comment 1'
}, {
total: 10,
comment: 'some other comment'
}];
this.getComments = function() {
return me.comments;
};
$interval(function() {
console.log('interval executed');
me.comments[0].total++;
}, 1000);
});

What is wrong with my bidirectional infinite scrolling in AngularJS?

I built an infinite scroll for a mobile web app built with AngularJS with the following extras features:
I built it to be bidirectional
This is for a mobile web app so I wanted it to unload out-of-view contents to avoid memory issues
Here is the jsfiddle link.
Now, I have a few questions and I also needs a small code review:
I am not familiar with promises, but then() seems to be executed before $digest. Thus, I need to delay my codes with $timeout. For me, it's a sign that something is wrong. I would like to remove the $timeout on lines 85 and 98. The $timeout on line 85 is a bit "hacky", I need to make sure it is executed ms after then() otherwise, it won't work and I don't know why.
I would like to know if it's considered a "good practice" to call a $scope method from a directive. In my code, I am calling $scope.init(value) from my directive.
Including jQuery for a position() is quite funny. Should I be using a services with a function that does what $.position() does?
I know those could be seperate questions but they are really related to my piece of code.
For those who do not want to click on the jsfiddle link, here is the code:
HTML:
<div id="fixed" scroll-watch="4" scroll-up="loadTop()" scroll-down="loadBottom()">
<ul>
<li data-id="{{i.id}}" ng-repeat="i in items" ng-class="calculateType(i.id)">{{i.id}}</li>
</ul>
</div>
JS:
function Main($scope, $timeout, $q) {
var cleanup = 5;
$scope.items = [];
//This is called from the scrollWatch directive. IMO, this shouldn't be a good idea
$scope.init = function(value) {
var deferred = $q.defer();
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
$scope.items = [{id: +value}];
$scope.loadTop();
$scope.loadBottom();
deferred.resolve();
}, 200);
return deferred.promise;
};
//This is only used to simulate different content's heights
$scope.calculateType = function(type) {
return 'type-' + Math.abs(type) % 4;
};
$scope.loadBottom = function() {
var deferred = $q.defer(),
counter;
if ($scope.items.length > 1) {
$scope.items.splice(0, cleanup);
}
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[$scope.items.length - 1]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.push({id: counter + i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
$scope.loadTop = function() {
var deferred = $q.defer(),
counter;
//Why can't I use this here?
//$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[0]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.unshift({id: counter - i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
//Why is this method needs to be delayed inside the directive? I would like to call it in loadTop()
$scope.removeBottom = function() {
$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
};
}
angular.module('scroll', []).directive('scrollWatch', ['$timeout', function($timeout) {
var lastScrollTop = 0;
return function($scope, elm, attr) {
var raw = elm[0];
$scope.init(attr.scrollWatch).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = $('li[data-id="' + attr.scrollWatch + '"]').position().top;
}, 300); //This value needs to be great enough so it is executed after the $scope.loadTop()'s resolve, for now, I know that I can set it to 300 but in real life app?
});
elm.bind('scroll', function() {
if (raw.scrollTop > lastScrollTop && raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
$scope.$apply(attr.scrollDown);
} else if (raw.scrollTop < lastScrollTop && raw.scrollTop === 0) {
var scrollHeight = raw.scrollHeight;
$scope.$apply(attr.scrollUp).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = raw.scrollHeight - scrollHeight;
//I would like to move this in the $scope.loadTop()
$scope.removeBottom();
});
});
}
lastScrollTop = raw.scrollTop;
});
};
}]);
Thank you
http://www.youtube.com/watch?v=o84ryzNp36Q
Is a great video on Promises, how to write them and how they work.
https://github.com/stackfull/angular-virtual-scroll
Is a directive replacement for ng-repeat that doesn't load anything not on screen It does from what I can tell exactly what your looking for.
I would have put this as a comment but you need 50 cred or reputation or whatever they call it.

Resources