calling a method from controller when $locationChangeStart is broadcasted - angularjs

In my controller I changed the url with varying parameters depending on geolocation changes of my map. This is my setLocation method:
$scope.setLocation = function(lat,lng){
$location.search('lat',lat);
$location.search('lng',lng);
$scope.$apply();
};
I have another method that loads location data on the map. This is my getProjectsByCenter:
$scope.getProjectsByCenter = function(){
var center = getProjectsByCenter();
$scope.setLocation(center.lat(),center.lng());
};
Once the location starts changing the browser saves all these url changes in its history, but when I click on the back button I can't figure out how to call my controller's method. Back Button does change the $location and the following broadcast listener is called. In fact the following broadcast listener is called everytime I setLocation(), but in this case everything is working as it should.
app.run(['$rootScope', '$location',
function ($rootScope, $location) {
//Client-side security. Server-side framework MUST add it's
//own security as well since client-based “security” is easily hacked
$rootScope.$on('$locationChangeStart', function (event, next, current) {
if( next !== current && (hasBackButtonBeenClicked() || scopeFunctionWasNotCalled()) ){
// fetch project again.
// $state.reload();
// $rootScope.$apply(); <--- doesn't work
console.log("location has changed...now find a way to call controller's $scope.getProjectsByCenter()...");
}
});
}]);
Any suggestions?

You can capture the change event in the following manner.
Add this controller in the destination page. The below method will capture the change event and you should be able to trigger the controller.
.controller('MyController', function() {
$scope.$on('$routeChangeSuccess', function () {
// Do your work
});
})
Hope this is what you are looking for.

Related

Refresh of scope after remote change to data

In my controller for a mpbile app based on Angular1 is have (for example) the following function:
var getItem = function() {
// Initialize $scope
$scope.url = "(url to get my data)";
$http.get($scope.url).success(function(data) {
$scope.itemDetails = data; // get data from json
});
};
getItem();
and this works just fine.. with one problem.. it doesnt update. Even if I switch pages and come back, if the scope hasnt changed, it doesnt reflect new data in the scope.
So, i built in an $interval refresh to look for changes in the scope, this works fine EXCEPT, when i leave the page to go to another, that interval keeps polling. This is obviously a bad idea in a mobile app where data and battery usage may be an issue.
So.. how can I keep checking the scope for 'live changes' when ON that page only OR what is best practice for the scope to refresh on data changes.
I have read about digests and apply but these still seem to be interval checks which I suspect will keep operation after switching pages.
Or on angular apps with live data, is constantly polling the API the 'thing to do' (admittedly the data the page pulls is only 629 bytes, but i have a few pages to keep live data on, so it will add up)
Thanks
When you create a controller, the function's in it are declared, but not run. and since at the end of the controller you are calling getItem(); it is run once.
Moving to another page, and coming back is not going to refresh it.
The only way to refresh is to call that function again, In your HTML or JS.
For example:
<button ng-click="getItem()">Refresh</button>
Really nice question, I have been wondering the same thing, so I checked a lot of related SO posts and wrote kind of a function that can be used.
Note: I am testing the function with a simple console.log(), please insert your function logic and check.
The concept is
$interval is used to repeatedly run the function($scope.getItem) for a period (in the below example for 1 second), A timeout is also actively running to watch for inactive time, this parameter is defined by timeoutValue (in the example its set to 5 seconds), the document is being watched for multiple events, when any event is triggered, the timeout is reset, if the timeoutValue time is exceeded without any events in the document another function is called where the interval is stopped. then on any event in the document after this, the interval is started back again.
var myModule = angular.module('myapp',[]);
myModule.controller("TextController", function($scope, $interval, $document, $timeout){
//function to call
$scope.getItem = function() {
console.log("function");
};
//main function
//functionName - specify the function that needs to be repeated for the intervalTime
//intervalTime - the value is in milliseconds, the functionName is continuously repeated for this time.
//timeoutValue - the value is in milliseconds, when this value is exceeded the function given in functionName is stopped
monitorTimeout($scope.getItem, 1000 ,5000);
function monitorTimeout(functionName, intervalTime, timeoutValue){
//initialization parameters
timeoutValue = timeoutValue || 5000;
intervalTime = intervalTime || 1000;
// Start a timeout
var TimeOut_Thread = $timeout(function(){ TimerExpired() } , timeoutValue);
var bodyElement = angular.element($document);
/// Keyboard Events
bodyElement.bind('keydown', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('keyup', function (e) { TimeOut_Resetter(e) });
/// Mouse Events
bodyElement.bind('click', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousemove', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('DOMMouseScroll', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousewheel', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('mousedown', function (e) { TimeOut_Resetter(e) });
/// Touch Events
bodyElement.bind('touchstart', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('touchmove', function (e) { TimeOut_Resetter(e) });
/// Common Events
bodyElement.bind('scroll', function (e) { TimeOut_Resetter(e) });
bodyElement.bind('focus', function (e) { TimeOut_Resetter(e) });
function TimerExpired(){
if(theInterval) {
$interval.cancel(theInterval);
theInterval = undefined;
}
}
function TimeOut_Resetter(e){
if(!theInterval){
theInterval = $interval(function(){
functionName();
}.bind(this), intervalTime);
}
/// Stop the pending timeout
$timeout.cancel(TimeOut_Thread);
/// Reset the timeout
TimeOut_Thread = $timeout(function(){ TimerExpired() } , timeoutValue);
}
var theInterval = $interval(function(){
functionName();
}.bind(this), intervalTime);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myapp">
<div ng-controller="TextController">
</div>
</div>
Depending on the router you are using, you have to tell the controller to reload when the route changed or updated, because the function you pass when declaring a controller is only a factory, and once the controller is constructed it won't run again because the router caches it (unless you tell angularjs to do so, which is rarely a good idea).
So your best bet is to use the router to reload the state when the route changes. You can do this using the router event change and update that is broadcast in the scope.
If you are using angularjs' router (a.k.a., ngRoute):
$scope.$on('$routeChangeUpdate', getItem);
$scope.$on('$routeChangeSuccess', getItem);
If you are using ui.router:
$scope.$on('$stateChangeUpdate', getItem);
$scope.$on('$stateChangeSuccess', getItem);
Note: in ui.router you can add cache: false on the state declaration and it'll prevent the controller and the view to be cached.

Angular UI Bootstrap Module Not Closing on Back Button

I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.
All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.
I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).
The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?
I know this is rather unusual case but any thoughts on it would be great.
Thanks!
#HankScorpio's solution is good, but I think there may be a simplified option now.
There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.
i.e. If only for the one page, then in your controller you'd have:
angular.module('app')
.controller('ctrl', ['$scope', '$uibModalStack', ctrl]);
function ctrl($scope, $uibModalStack) {
$scope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:
angular.module('app')
.run(['$rootScope', '$uibModalStack', setupUibModal]);
setupUibModal($rootScope, $uibModalStack) {
$rootScope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
Here is the simple solution when using ui-router for state change
Closing modal popup on the back button click in angularjs
App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function (event) {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
})
}]);
hope this will save lot of time for people who are breaking heads
I've run into this same problem. Here's how I fixed it.
1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open().
Here you go, you can have the one I wrote:
(function () {
'use strict';
var theModule = angular.module('services.modalService', ['ui.bootstrap']);
theModule.factory('ModalService', function ($modal) {
var service = {};
var currentModal;
var clearModal = function () {
currentModal = undefined;
};
service.getCurrentModal = function () {
return currentModal;
};
service.open = function (options) {
currentModal = $modal.open(options);
currentModal.result['finally'](clearModal);
return currentModal;
};
return service;
});
}());
2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.
$scope.$on('$routeChangeStart', function(){
var currentModal = ModalService.getCurrentModal();
if(angular.isDefined(currentModal)){
currentModal.dismiss('cancel');
}
});
3) Your modals should now close when a user hits back.
4) Enjoy.
IMPROVEMENT:
I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.
1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart
Here is an example of a state that opens (and closes) it's modal:
.state('product.detail', {
url: '/detail/{productId}',
onEnter: /*open-modal logic*/,
onExit: ['ModalService', function (ModalService) { ModalService.close()} ]
})
I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:
a. Add an isStateful flag to modalService.open(...):
service.open = function (options, isStateful) {
currentModal = $uibModal.open(options);
currentModal.result.finally(function () {
clearModal(isStateful);
});
return currentModal;
};
so that clearModal will return to previous state:
var clearModal = function (isStateful) {
currentModal = undefined;
if (isStateful)
$state.go('^');
};
Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):
service.close = function() {
if (currentModal) {
currentModal.dismiss().then(function () {
clearModal();
})
}
}
The benefits of this are that back button functionality is controlled at state config level, not through a listener.

What do I need to do to get $broadcast running?

I have been working with the excelent ngStorage plugin for angular.
When setting it up you can declare a $scope-node connected to the localstorage like this:
$scope.$store = $localStorage;
$scope.$store is now accessible in all controllers etc.
I want to remove some stuff from localstorage and access it using broadcast instead.
In my init I performed:
$scope.taskarr = [];
$rootScope.$broadcast('taskarrbroad',$scope.taskarr);
What is required in order to add, remove and $watch this array, none of the mentioned seem to work.
Here, nothing happens
controller('textController', function($scope,$routeParams){
$scope.$watch('taskarrbroad.length', function(){
console.log($scope.taskarr.map(function(task){
return task.content;
}).join('\n'));
})
})
Here I can access $scope.taskarr and update it, but the view isn't updated. $scope.$apply() didn't help either (the timeout is because it's already within a digest.
controller('stateSwitchController', function($scope, $routeParams, $timeout){
$scope.taskarr = $scope.$store[$routeParams.state].taskarr || [];
console.log($scope.taskarr);
$timeout(function() {
$scope.$apply();
})
}).
$broadcast is a way to send events to other parts of your application. When you broadcast an event, someone else has to listen to that even with $on(). Something like:
// Some controller
$rootScope.$broadcast('my-event', eventData);
// Some other controller
$scope.$on('my-event', function() {
console.log('my-event fired!')
});
$watch is something else, it's not an event listener per se, it's a way to attach a function that gets called when that value changes, and that value has to be on the scope. So your watch should look like this:
$scope.$watch('taskarr.length', function(){
});
Since you've named the array taskarr on the scope.

AngularJS loading progress bar

When using AngularJS and doing a redirect using $location.path('/path') the new page takes a while to load, especially on mobile.
Is there a way to add a progress bar for loading? Maybe something like YouTube has?
For a progress bar as YouTube has, you can take a look at ngprogress. Then just after the configuration of your app (for example), you can intercept route's events.
And do something like:
app.run(function($rootScope, ngProgress) {
$rootScope.$on('$routeChangeStart', function() {
ngProgress.start();
});
$rootScope.$on('$routeChangeSuccess', function() {
ngProgress.complete();
});
// Do the same with $routeChangeError
});
Since #Luc's anwser ngProgress changed a bit, and now you can only inject ngProgressFactory, that has to be used to create ngProgress instance. Also contrary to #Ketan Patil's answer you should only instantiate ngProgress once:
angular.module('appRoutes', ['ngProgress']).run(function ($rootScope, ngProgressFactory) {
// first create instance when app starts
$rootScope.progressbar = ngProgressFactory.createInstance();
$rootScope.$on("$routeChangeStart", function () {
$rootScope.progressbar.start();
});
$rootScope.$on("$routeChangeSuccess", function () {
$rootScope.progressbar.complete();
});
});
if it is the next route that takes time to load e.g. making ajax call before the controller is run (resolve config on route) then make use of $route service's $routeChangeStart, $routeChangeSuccess and $routeChangeError events.
register a top level controller (outside ng-view) that listens to these events and manages a boolean variable in its $scope.
use this variable with ng-show to overlay a "loading, please wait" div.
if the next route loads fast (i.e. its controller runs quickly) but data that are requested by the controller take a long to load then, i'm afraid, you have to manage the visibility state of spinners in your controller and view.
something like:
$scope.data = null;
$http.get("/whatever").success(function(data) {
$scope.data = data;
});
<div ng-show="data !== null">...</div>
<div ng-show="data === null" class="spinner"></div>
use angular-loading-bar
Standalone demo here ..
https://github.com/danday74/angular-loading-bar-standalone-demo
Here is a working solution which I am using in my application. ngProgress is the best library out there for showing load-bars when changing urls.
Remember to inject the ngProgressFactory instead of ngProgress, as opposed to Luc's solution.
angular.module('appRoutes', []).run(function ($rootScope, ngProgressFactory) {
$rootScope.$on("$routeChangeStart", function () {
$rootScope.progressbar = ngProgressFactory.createInstance();
$rootScope.progressbar.start();
});
$rootScope.$on("$routeChangeSuccess", function () {
$rootScope.progressbar.complete();
});
});
Update Nov-2015 - After analyzing this approach with chrome timings, I have observed that this would not be the correct way for adding a loading bar. Sure, the loading bar will be visible to visitors,but it will not be in sync with actual page load timings.

How do I broadcast from the http interceptor?

Using AngularJS 1.2
My interceptor looks like this:
$httpProvider.interceptors.push(['$q', '$log', '$rootScope', function ($q, $log, $rootScope) {
return {
'request': function(config) {
$rootScope.$broadcast('spin');
console.info('request!');
return config || $q.when(config);
},
...
In my nav controller (which handles and binds the loader/spinner to the view):
$rootScope.$watch('spin', function(event) {
console.info('spin');
$scope.spinner++;
});
The broadcast seems to happen only once at the end of all the responses received, even though I can see many request! in the console log.
How must I manage my global spinner/loader?
EDIT
I wish to show a loader/spinner in my navbar whenever data is being loaded.
The $watch function doesn't listen for broadcast messages. It watches for changes on the scope. In this case, you are calling a function whenever $rootScope.spin changes, which gets called (by default) immediately, which is why you got called once.
The $on function is what you want here, as it is what will listen to broadcast events.
$rootScope.$on('spin', function(msg, data) {
console.info('spin');
$scope.spinner++;
});
I've put together a complete working example if you are curious:
http://codepen.io/BrianGenisio/pen/wIBHz
Instead of using watcher you should just use on in the module run function
angular.module('test',[]).run(['$rootScope' function ($rootScope) {
$rootScope.$on("$spin", function () {
// set the spinner here
});
}]);

Resources