Apply loading spinner during ui-router resolve - angularjs

resolve property of $routeProvider allows to execute some jobs BEFORE corresponding view is rendered.
What if I want to display a spinner while those jobs are executed in order to increase user experience?
Indeed, otherwise the user would feel the application has been blocked since no view elements were displayed for some milliseconds > 600 for instance.
Of course, there was the way to define a global div element out of the current view to display in order to display the spinner thanks to the $scope.$rootChangeStart function.
But I don't want to hide the whole page with just a poor spinner in the middle.
I want some pages of my webapp differ regarding the way the loading is displayed.
I came across this interesting post containing the exact issue I described above:
That approach results in a horrible UI experience. The user clicks on
a button to refresh a list or something, and the entire screen gets
blanketed in a generic spinner because the library has no way of
showing a spinner just for the view(s) that are actually affected by
the state change. No thanks.
In any case, after I filed this issue, I realised that the "resolve"
feature is an anti-pattern. It waits for all the promises to resolve
then animates the state change. This is completely wrong - you want
your transition animations between states to run parallel to your data
loads, so that the latter can be covered up by the former.
For example, imagine your have a list of items, and clicking on one of
them hides the list and shows the item's details in a different view.
If we have an async load for the item details that takes, on average,
400ms, then we can cover up the load almost entirely in most cases by
having a 300ms "leave" animation on the list view, and a 300ms "enter"
animation on the item details view. That way we provide a slicker feel
to the UI and can avoid showing a spinner at all in most cases.
However, this requires that we initiate the async load and the state
change animation at the same moment. If we use "resolve", then the
entire async animation happens before the animation starts. The user
clicks, sees a spinner, then sees the transition animation. The whole
state change will take ~1000ms, which is too slow.
"Resolve" could be a useful way to cache dependencies between
different views if it had the option not to wait on promises, but the
current behaviour, of always resolving them before the state change
starts makes it almost useless, IMO. It should be avoided for any
dependencies that involve async loads.
Should I really stop using resolve to load some data and rather start loading them in the corresponding controller directly? So that I can update the corresponding view as long as the job is executed and in the place I want in the view, not globally.

You can use a directive that listens on $routeChangeStart and for example shows the element when it fires:
app.directive('showDuringResolve', function($rootScope) {
return {
link: function(scope, element) {
element.addClass('ng-hide');
var unregister = $rootScope.$on('$routeChangeStart', function() {
element.removeClass('ng-hide');
});
scope.$on('$destroy', unregister);
}
};
});
Then you place it on the specific view's loader, for example:
View 1:
<div show-during-resolve class="alert alert-info">
<strong>Loading.</strong>
Please hold.
</div>
View 2:
<span show-during-resolve class="glyphicon glyphicon-refresh"></span>
The problem with this solution (and many other solutions for that matter) is that if you browse to one of the routes from an external site there will be no previous ng-view template loaded, so your page might just be blank during resolve.
This can be solved by creating a directive that will act as a fallback-loader. It will listen for $routeChangeStart and show a loader only if there is no previous route.
A basic example:
app.directive('resolveLoader', function($rootScope, $timeout) {
return {
restrict: 'E',
replace: true,
template: '<div class="alert alert-success ng-hide"><strong>Welcome!</strong> Content is loading, please hold.</div>',
link: function(scope, element) {
$rootScope.$on('$routeChangeStart', function(event, currentRoute, previousRoute) {
if (previousRoute) return;
$timeout(function() {
element.removeClass('ng-hide');
});
});
$rootScope.$on('$routeChangeSuccess', function() {
element.addClass('ng-hide');
});
}
};
});
The fallback loader would be placed outside the element with ng-view:
<body>
<resolve-loader></resolve-loader>
<div ng-view class="fadein"></div>
</body>
Demo of it all: http://plnkr.co/edit/7clxvUtuDBKfNmUJdbL3?p=preview

i think this is pretty neat
app.run(['$rootScope', '$state',function($rootScope, $state){
$rootScope.$on('$stateChangeStart',function(){
$rootScope.stateIsLoading = true;
});
$rootScope.$on('$stateChangeSuccess',function(){
$rootScope.stateIsLoading = false;
});
}]);
and then on view
<div ng-show='stateIsLoading'>
<strong>Loading.</strong>
</div>

To further Pranay's answer this is how I did it.
JS:
app.run(['$rootScope',function($rootScope){
$rootScope.stateIsLoading = false;
$rootScope.$on('$routeChangeStart', function() {
$rootScope.stateIsLoading = true;
});
$rootScope.$on('$routeChangeSuccess', function() {
$rootScope.stateIsLoading = false;
});
$rootScope.$on('$routeChangeError', function() {
//catch error
});
}]);
HTML
<section ng-if="!stateIsLoading" ng-view></section>
<section ng-if="stateIsLoading">Loading...</section>

I'm two years late to this, and yes these other solutions work but I find it easier to just handle all this in a just a run block like so
.run(['$rootScope','$ionicLoading', function ($rootScope,$ionicLoading){
$rootScope.$on('loading:show', function () {
$ionicLoading.show({
template:'Please wait..'
})
});
$rootScope.$on('loading:hide', function () {
$ionicLoading.hide();
});
$rootScope.$on('$stateChangeStart', function () {
console.log('please wait...');
$rootScope.$broadcast('loading:show');
});
$rootScope.$on('$stateChangeSuccess', function () {
console.log('done');
$rootScope.$broadcast('loading:hide');
});
}])
You don't need anything else. Pretty easy huh. Here's an example of it in action.

In ui-router 1.0 $stateChange* events are deprecated. Use transition hook instead. See migration guide below for more details.
https://ui-router.github.io/guide/ng1/migrate-to-1_0#state-change-events

The problem with '$stateChangeStart' and '$stateChangeSuccess' is "$rootScope.stateIsLoading" doesn't get refreshed when you go back to last state.
Is there any solution on that?
I also used:
$rootScope.$on('$viewContentLoading',
function(event){....});
and
$rootScope.$on('$viewContentLoaded',
function(event){....});
but there is the same issue.

Related

ng-show directive takes too long to update the dom after trigger

The app has a controller, that uses a service to create an instance of video player. The video player triggers events to show progress every few seconds. When the video reaches to a certain point, I want to show a widget on top of the video player.
The view has the widget wrapped in ng-show directive.
It takes more then 60 seconds for the dom element to receive the signal to remove the ng-hide class after the event has been triggered and the values have been populated.
If I try to implement this using the plain dom menthod (like document.getElementById(eleId).innerHTML = newHTML), the update is instant.
What am I doing wrong? Here is the complete sequence in code:
Controller:
MyApp.controller('SectionController', ['$scope', 'PlayerService'], function($scope, PlayerService){
$scope.createPlayer = function() {
PlayerService.createPlayer($scope, wrapperId);
}});
Service:
MyApp.service('PlayerService', [], function(){
this.createPlayer=function(controllerScope, playerWrapper){
PLAYER_SCRIPT.create(playerWrapper) {
wrapper : playerWrapper,
otherParam : value,
onCreate : function(player) {
player.subscribe(PLAY_TIME_CHANGE, function(duration){
showWidget(controllerScope, duration);
})
}
}
}
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
}
}});
View:
<div ng-show="showWidget"> <div class="wdgt">{{widgetData.stuff}}</div> </div>
Solved it! $scope.$apply() did the trick.
My guess is, due to other complex logic ad bindings inside the app, there was a delay in computing the change by angular the default way.
#floribon Thanks for the subtle hint about "complex angular stuff".
The code inside the service function changed to:
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
$rootScope.$apply();
}}
Do you have complex angular stuff within your hidden view?
You should try to use ng-if instead of ng-show, the difference being that when the condition is false, ng-if will remove the element from the DOM instead of just hidding it (which is also what you do in vanilla JS).
When the view is simply hidden using ng-show however, all the watchers and bindings within it keep being computed by Angular. Let us know if ng-if solve your problem, otherwise I'll edit my answer.

AngularJS loading message/please wait

How can I achieve the following in AngularJS:
If I have my page that will show some widgets and each widget has a button called refresh. When the button is clicked then the content of that widget is reloaded from server. While the content is reloaded I want to show the user a message within that widget, please wait ... with a possible fading effect.
How can I achieve that in AngularJS?
I kind of taught about having a common service for that purpose and then somehow each widget controller will use that service or something like that, maybe also a directive that will show the actual loading/please wait message?
What is your advise?
P.S. There should be also a loading/please wait message with fading for the whole page, when the route is changing ... like switching between pages.
In my recent project I'm using https://github.com/cgross/angular-busy
Very nice thing, all you have to do is put your promise into $scope, and then add cg-busy attr to your element which should have spinner (beside registering module obviously):
Controller:
$scope.myPromise = restangular.get('something',12).then(function(response) { ... })
Html:
<div cg-busy="myPromise"></div>
You can also customize template that's gonna be displayed (which includes spinner and text message).
there are implementations of this available on github : angular-spinner or angular-sham-spinner. Read this BLOG which details how the spinner works with angularjs
if you want to implement it yourself to be reusable...
app.directive("spinner", function(){
return: {
restrict: 'E',
scope: { enable: "=" },
template: '<div class="spinner" ng-show="enable"><img src="content/spinner.gif"></div>'
}
});
i havent tested the code but directive wont be more complex than above...
As harish point out the right way would be a directive, but theres no need if you want of include another dependency, you could do something like this
You can create a nice CSS3 only loading (so not images required) animation with the help of CssLoad
Create a directive with a linking function so you can call and stop the animations within your controller the angular way:
.directive('appLoading', function(){
return {
restrict: 'E',
templateUrl: 'template-file.html', // or template: 'template html code inline' Display none to the code is important so is not visible if youre not caling the methods
replace: true,
link: function(scope, elem) {
scope.$on('app-start-loading', function(){
elem.fadeIn(); //asumming you have jquery otherwise play with toggleClass and visible and invisible classes
});
scope.$on('app-finish-loading', function(){
elem.fadeOut();
});
}
}
})
Include in your html code the directive: <app-loading></app-loading>
Now all you need to do is call the scope methods in your controller like this:
$scope.$broadcast('app-start-loading'); // to start the loading animation
$scope.$broadcast('app-finish-loading'); // to stop the animation
NOTE: if all your widgets share a scope, the loading may be triggered in all of them

Angularjs menu login logout loading

I am using Angularjs in a project.
For login logout I am setting a scope variable like below:
$scope.showButton = MyAuthService.isAuthenticated();
In markup its like
<li ng-show="showLogout">Logout</li>
When I logout it redirect to the login page but logout menu doesn't disappear.
Also tried like this:
$scope.showButton = MyAuthService.isAuthenticated();
In markup:
<li ng-class=" showLogout ? 'showLogout' : 'hideLogOut' ">Logout</li>
Seems scope change is not reflecting in my view, but when I reload page "logout menu" disappears as expected.
I also tried with directives like below:
MyApp.directive('logoutbutton', function(MyAuthService) {
return {
restrict: 'A',
link: function(scope, element, attrs, controller) {
attrs.$observe('logoutbutton', function() {
updateCSS();
});
function updateCSS() {
if (MyAuthService.isAuthorized()) {
element.css('display', 'inline');
} else {
element.css('display', 'none');
}
}
}
}
});
No luck with that too.
How can I hide it when the logout is successful and also after successful login how can I show "logout button"?
Setup a watch on MyAuthService.isAuthenticated() and when that fires, set your scope variable to the result of that service call. In your first example, the scope variable is only getting set once when the controller is initialized (I am assuming that's where it is being run). You can set the watch up in the controller or, if you want to use a directive, in the directive link function.
Something like this:
$scope.$watch(MyAuthService.isAuthenticated, function(newVal, oldVal){
$scope.showButton = newVal;
});
Edit: After read the MarkRajcok comment I realized that this solution is coupling view from business logic layer, also it exposes the service variable to be changed outside the service logic, this is undesirable and error prone, so the $scope.$watch solution proposed by BoxerBucks it's probably better, sorry.
You can use $scope.$watch as in the BoxerBucks answer, but I think that using watchers isn't proper for services, because usually you want to access services variables in differents controllers expecting that when you change that services variables, all the controllers that inject that service will be automatically updated, so I believe that this is a good way to solve your problem:
In your MyAuthServices do this:
app.service('MyAuthService', function(...){
var MyAuthServiceObj = this;
this.authenticated=false; // this is a boolean that will be modified by the following methods:
// I supose that you have methods similar to these ones
this.authenticateUser(...){
...
// At some point you set the authenticated var to true
MyAuthServiceObj.authenticated = true;
}
this.logout(){
....
// At some point you set the authenticated var to false
MyAuthServiceObj.authenticated = false;
}
});
Then in your controller do this:
$scope.myAuthService = MyAuthService;
finally in your html:
ng-show="myAuthService.authenticated"
And this should work without using a watcher like in BoxerBucks answer.
Check this excellent video about AngularJS providers, to understand how to use services properly.

How to hide modal window on $route.reload (using angularjs-ui)?

I have a form in a modal window which adds data to a database. Upon successful addition, I am calling the $route.reload() function to refresh the underlying page, however, my modal window is still shown.
$scope.add = function() {
$http.post('/api/contact', $scope.form).
success(function(data) {
$scope.showModal = false;
$route.reload();
});
};
The modal is the standard angularjs-ui modal:
<div modal="showModal" close="close()" options="opts"> etc </div>
I tried to add $scope.showModal = false; at the beginning of my controller but that didn't help either.
Here's a code in plnkr:
http://plnkr.co/edit/xz6gJRTBisVCeM0gNLx6?p=preview
Note that I am aware that if you comment out the $route.reload() option the items in the list get updated, however in my app I am writing things into a database so I'd need the $route.reload as I need to re-query the DB.
You need a timeout before applying the $route.reload() because that's trying to fire off before the browser has time to update the view. This works: http://plnkr.co/edit/E7oraQ?p=preview
Also, unless you're dealing with non-angular stuff, there isn't any reason to reload just to write to the DB. You should be able to re-query within the controller.

AngularJS modal window directive

I'm trying to make a directive angularJS directive for Twitter Bootstrap Modal.
var demoApp = angular.module('demoApp', []);
demoApp.controller('DialogDemoCtrl', function AutocompleteDemoCtrl($scope) {
$scope.Langs = [
{Id:"1", Name:"ActionScript"},
{Id:"2", Name:"AppleScript"},
{Id:"3", Name:"Asp"},
{Id:"4", Name:"BASIC"},
{Id:"5", Name:"C"},
{Id:"6", Name:"C++"}
];
$scope.confirm = function (id) {
console.log(id);
var item = $scope.Langs.filter(function (item) { return item.Id == id })[0];
var index = $scope.Langs.indexOf(item);
$scope.Langs.splice(index, 1);
};
});
demoApp.directive('modal', function ($compile, $timeout) {
var modalTemplate = angular.element("<div id='{{modalId}}' class='modal' style='display:none' tabindex='-1' role='dialog' aria-labelledby='myModalLabel' aria-hidden='true'><div class='modal-header'><h3 id='myModalLabel'>{{modalHeaderText}}</h3></div><div class='modal-body'><p>{{modalBodyText}}</p></div><div class='modal-footer'><a class='{{cancelButtonClass}}' data-dismiss='modal' aria-hidden='true'>{{cancelButtonText}}</a><a ng-click='handler()' class='{{confirmButtonClas}}'>{{confirmButtonText}}</a></div></div>");
var linkTemplate = "<a href='#{{modalId}}' id= role='button' data-toggle='modal' class='btn small_link_button'>{{linkTitle}}</a>"
var linker = function (scope, element, attrs) {
scope.confirmButtonText = attrs.confirmButtonText;
scope.cancelButtonText = attrs.cancelButtonText;
scope.modalHeaderText = attrs.modalHeaderText;
scope.modalBodyText = attrs.modalBodyText;
scope.confirmButtonClass = attrs.confirmButtonClass;
scope.cancelButtonClass = attrs.cancelButtonClass;
scope.modalId = attrs.modalId;
scope.linkTitle = attrs.linkTitle;
$compile(element.contents())(scope);
var newTemplate = $compile(modalTemplate)(scope);
$(newTemplate).appendTo('body');
$("#" + scope.modalId).modal({
backdrop: false,
show: false
});
}
var controller = function ($scope) {
$scope.handler = function () {
$timeout(function () {
$("#"+ $scope.modalId).modal('hide');
$scope.confirm();
});
}
}
return {
restrict: "E",
rep1ace: true,
link: linker,
controller: controller,
template: linkTemplate
scope: {
confirm: '&'
}
};
});​
Here is JsFiddle example http://jsfiddle.net/okolobaxa/unyh4/15/
But handler() function runs as many times as directives on page. Why? What is the right way?
I've found that just using twitter bootstrap modals the way the twitter bootstrap docs say to is enough to get them working.
I am using a modal to house a user edit form on my admin page. The button I use to launch it has an ng-click attribute that passes the user ID to a function of that scope, which in turn passes that off to a service. The contents of the modal is tied to its own controller that listens for changes from the service and updates values to display on the form.
So.. the ng-click attribute is actually only passing data off, the modal is still triggered with the data-toggle and href tags. As for the content of the modal itself, that's a partial. So, I have multiple buttons on the page that all trigger the single instance of the modal that's in the markup, and depending on the button clicked, the values on the form in that modal are different.
I'll take a look at my code and see if I can pull any of it out to build a plnkr demo.
EDIT:
I've thrown together a quick plunker demo illustrating essentially what I'm using in my app: http://embed.plnkr.co/iqVl0Wb57rmKymza7AlI/preview
Bonus, it's got some tests to ensure two password fields match (or highlights them as errored), and disables the submit button if the passwords don't match, or for new users username and password fields are empty. Of course, save doesn't do anything, since it's just a demo.
Enjoy.
There is a working native implementation in AngularStrap for Bootstrap3 that leverages ngAnimate from AngularJS v1.2+
Demo : http://mgcrea.github.io/angular-strap/##modals
You may also want to checkout:
Source : https://github.com/mgcrea/angular-strap/blob/master/src/modal/modal.js
Plunkr : http://plnkr.co/edit/vFslNmBAoKPVXtdmBXgv?p=preview
Well, unless you want to reinvent this, otherwise I think there is already a solution.
Check out this from AngularUI. It runs without twitter bootstrap.
I know it might be late but i started trying to figure out why the handler got called several times as an exercise and I couldn't stop until done :P
The reason was simply that each div you created for each modal had no unique id, once I fixed that everything started working. Don't ask me as to what the exact reason for this is though, probably has something to do with the $('#' + scope.modalId).modal() call.
Just though I should post my finding if someone else is trying to figure this out :)

Resources