I'm trying to$broadcast some data from one controller to another using the$rootScope .
It appears to work fine if I use a trigger like an ng-click to run the function that will broadcast but how do it without that?
As you can see in the fiddle, I have the broadcast in a $scope.cast function so why is it not working if I run the function like this: $scope.cast(); ?
Fiddle: https://jsfiddle.net/kjgj7Ldz/19/
I need this because I am getting some data in the first controller and when that finishes, I want to automatically broadcast it without ng-click, ng-change or any other triggers.
Is $broaadcast a wrong thing to do in this scenario? If so, how can I achieve data communication between those two controllers?
You can avoid using scope for commutication between controllers by creating a simple pub-sub service that handles the communtication channel for you. For example it can deliver all messages for late subscribers. Demo.
app.service('MQ', function() {
var listeners = [],
messages = [];
return {
pub: function(message) {
listeners.slice(0).forEach(function(listener) {
try {
listener(message)
} catch (ignored) {
console.log(ignored)
}
})
// save message for late subscribers.
messages.push(message)
},
sub: function(listener) {
// deliver all messages
messages.slice(0).forEach(function(message) {
try {
listener(message)
} catch (ignored) {
console.log(ignored)
}
})
// save listener
listeners.push(listener)
// create unbinder
return function() {
listeners.splice(listeners.indexOf(listener), 1)
}
}
}
})
app.controller('Controller1', ['$scope', 'MQ', function($scope, MQ) {
MQ.pub('John Snow')
$scope.cast = function() {
MQ.pub(Math.random())
}
}]);
app.controller('Controller2', ['$scope', 'MQ', function($scope, MQ) {
var unsub = MQ.sub(function(message) {
$scope.message = message
})
// clean-up bindings on scope destroy.
$scope.$on('$destroy', unsub)
}]);
Related
I am working on a application originally created with backbone and jQuery, however due to client requirement, new modules are built with angular. Routing of the application is handled with backbone route and we have successfully integrated angular modules.
The actual problem is, I need to retrieve the current instance of a module in angular and execute a function from the controller of that module based on actions handled by a backbone controller.
Here is what my angular module and controller looks like:
//In chat.module.js
( function () {
angular
.module( 'chat.module', [] );
})();
//In chat.controller.js
(function () {
angular
.module('chat.module')
.controller('chat.controller', ['profileFactory', '$filter', '$q', '$timeout', 'Position', 'Chat', chat]);
function chat(profileFactory, $filter, $q, $timeout, Position, Chat) {
var vm = this;
vm.initChatFlag = false;
vm.initChat = initChat;
vm.setInformation = setInformation;
function setInformation() {
//handle business logic here
}
...
In backbone, the module is created as follows:
chatmodule: function () {
var self = this;
var element = angular.element(document.querySelector('#modalCallback'));
var chat = angular.element(document.querySelector('#chatModule'));
var isInitializedChat = chat.injector();
var isInitialized = element.injector();
if (!isInitialized) {
angular.bootstrap($('#modalCallback'), ['app']);
}
if (!isInitializedChat) {
angular.bootstrap($('#chatModule'), ['app']);
}
//TODO: chat.controller.setInformation() get access to fields like chat.controller.initChatFlag etc
The main app module is defined thus:
(function(){
angular
.module('app',[
'callback',
'ui.bootstrap',
'720kb.datepicker',
'ngLocale',
'directives.module',
'interceptor',
'directive.loading',
'angularUtils.directives.dirPagination',
'blog.module',
'profile.module',
'filters.module',
'chat.module',
'ui.toggle',
]);
})();
The AngularJS $injector is where a lot of the magic happens, so if you expose that outside of the AngularJS code you can hook it up to non-AngularJS code like the following:
//A simple AngularJS service:
app.service('myService', function() {
this.message = "This is my default message.";
});
//Expose the injector outside the angular app.
app.run(function($injector, $window) {
$window.angularInjector = $injector;
});
//Then use the injector to get access to the service.
//Make sure to wrap the code in a `$apply()` so an
//AngularJS digest cycle will run
function nonAngularEventHandler() {
angularInjector.invoke(function(myService, $rootScope) {
$rootScope.$apply(function() {
myService.message = "Now this is my message."
});
});
}
Edit: Alternatively, simplify the call like so.
//Instead of exposing the $injector directly, wrap it in a function
//which will do the $apply() for you.
app.run(function($injector, $window, $rootScope) {
$window.callInMyAngularApp = function(func) {
$rootScope.$apply(function() {
$injector.invoke(func);
});
}
});
//Then call that function with an injectable function like so.
function nonAngularClick() {
callInMyAngularApp(function(myService) {
myService.message = "Now this is my message."
});
}
//And remember if you're minifying, you'll want the minify-safe
//version of the injectable function like this
function nonAngularClick() {
callInMyAngularApp(['myService', function(myService) {
myService.message = "Now this is my message."
}]);
}
Update: (last one I promise!)
The above will work fine, but you might want to consider exposing a well-defined API instead of a generic injectable interface. Consider the following.
//Now I have a limited API defined in a service
app.service("myExternalApi", function($rootScope, myService) {
this.changeMyMessage = function(message) {
$rootScope.$apply(function() {
myService.message = message;
});
};
});
//And I just expose that API
app.run(function($window, myExternalApi) {
$window.myExternalApi = myExternalApi;
});
//And the call from outside of angular is much cleaner.
function nonAngularClick() {
myExternalApi.changeMyMessage("Now this is my message.");
}
I was able to get access to the controller using answer from this post - https://stackoverflow.com/a/21997129/7411342
var Chat = angular.element(document.querySelector('#chatModule')).scope();
if(!Chat) return;
if(Chat.chatCtrl.initChatFlag) {
Chat.chatCtrl.setInformation();
}else{
console.log('Chat has not been initialized');
}
I am wrapping a web service client into an angular service. This particular client works emmiting events for particular updates like so:
app.service('AngularClient', function () {
this.info = null
this.login = function () {
client.login(loginInfo).then(function (loggedClient) {
loggedClient.on('newInfo', function (info) {
this.info = info
})
})
}
})
A controller uses this service and binds it to its $scope:
app.controller('Ctrl', function (AngularClient, $scope) {
$scope.client = AngularClient
})
However, anytime the 'newInfo' event gets fired angular doesn't automatically trigger a digest cycle, so I can't control when the info gets updated in the UI. What's the angular way of making sure this happens everytime?
If you want to keep receiving updates to the login event, you could do something like this:
app.service('AngularClient', function () {
this.info = null
this.loginCallback = null
this.login = function () {
client.login(loginInfo).then(function (loggedClient) {
loggedClient.on('newInfo', function (info) {
this.info = info
if (this.loginCallback) this.loginCallback(info);
})
})
}
})
app.controller('Ctrl', function (AngularClient, $scope) {
$scope.client = AngularClient
AngularClient.loginCallback = function(info, err){ // optional error
$scope.user.property = info.property;
$scope.$apply();
}
})
Angular will not be aware that it should digest again if on custom async methods created by your code.
A way to get around that is to use $scope.$apply(). However $scope is not exposed to your service. So on your angular controller, you can wrap the listened function of your event into an $apply();
$scope.$apply(function () {
//your code here.
});
http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
Is there a "nice" way to set the server's date to an angularJS app?
Let's say that I have an API route to get this, sort of /api/date and I can call it with an angular service called dateService:
angular.module('myApp').service('DateService', ['$resource', function ($resource) {
return $resource('/api/date', { }, {
});
}]);
I need to get the date as soon as the app starts because my html file uses a function based on that date to display something. Otherwise I'll get an "undefined" error everytime I call it.
Thanks in advance
Have the back-end server return a timestamp in UTC.
Parse the timestamp value with var date = new Date(serverTimestampValue);
You now have a JS Date object that can be used with Angular Date filters.
If you are using ui-router for routing and if you need to learn the timestamp before showing anything to user you can use the resolve property of ui-router. Basically it resolves what you need before activating a state, and if you retrieve your server time in your parent state you can be sure that you will have the timestamp before anything starts in your application. Here is an example how to do it:
angular.module("yourApp").config(["$stateProvider", function ($stateProvider) {
$stateProvider
.state("topState", {
abstract: true,
template: "<ui-view></ui-view>",
controller: ["$rootScope", "serverTimestamp", function ($rootScope, serverTimestamp) {
$rootScope.serverTime = new Date(serverTimestamp);
//Do what you need to do with the server time, from now on you will have access to server time from each controller in your app.
}],
resolve: {
serverTimestamp: ["", function (bakkalBrandService) {
return yourService.retrieveTimestampFromServer();
}]
}
})
}])
You can do that in app.run
var app = angular.module('myApp',[]);
app.run(function ($rootScope, $state, DateService) {
// call date service and save it in $rootScope
});
You can delay the execution of angular app by manually bootstrapping it rather than auto initialization.
https://docs.angularjs.org/guide/bootstrap
This way you can do the following to get the server settings and have it available when your angular app starts.
//Define a global settings object. add proper namespace as per your app.
var myServerConfig = (function () {
var settings = {
someDefaultSetting: 'someDefaultValue'
};
return {
init: function (serverSettings) {
settings = $.extend(settings, serverSettings);
},
settings: settings
}
})();
//Define angular wrapper for your global object
var serverSettings = angular.module('server.settings', []);
serverSettings.factory('serverSettings', ['$window', function () {
return $window.myServerConfig.settings;
}]);
//On dom ready get all settings and bootstrap angular app
$(document).ready(function () {
//You will have to show some animation if you are making ajax call
//also if errors out handle that scenario
//Other option is to render settings from server on page load as mentioned in other answers
$.ajax({
type: 'GET',
url: '/api/settings',
error: function (error) {
//show user message that app cannot be loaded
},
success: function (setting) {
myServerConfig.init(setting);
bootStrapMyApp();
}
});
function bootStrapMyApp() {
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
}
angular.module('myApp').service('DateService', ['$resource', function ($resource) {
this.date = null;
var self = this;
$resource('/api/date', { }, {
}).then(function(data){
self.date = data.date;
});
}]);
You can assign it to a service variable after you get it.
angular.module('myApp').service('DateService', ['$resource', function ($resource) {
this.date = null;
var self = this;
$resource('/api/date', { }, {
}).then(function(data){
self.date = data.date;
});
}]);
Then call the service to get it when you need it.
$scope.date = function() {
return DateService.date;
}
Scenario
I have a service UserService that maintains the (boolean) sessionStatus of the user.
The view conditionally shows [LOGOUT] on ng-show=sessionStatus (i.e. if not logged in (false), no show).
sessionStatus of ViewController should therefore always match that of UserService ... right?
If you click [LOGOUT] when it's visible, it does some loggy outty things, sessionStatus value changes, and view should update with new outcome of ng-show....
Problem
Currently, clicking logout does not seem to update var UserService.sessionStatus?
How do I keep $scope.sessionStatus updated when UserService.logout() occurs and changes UserService.sessionStatus?
How do I map the changing $scope to ng-show?
Files
View
<a ng-show="sessionStatus" ng-click="logout()">Logout</a>
ViewController
app.controller('AppController', function($scope, $interval, $http, UserService) {
$scope.logout = function() { UserService.logout(); }
// This ain't working
$scope.$watch(UserService.sessionStatus, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
});
UserService
NB: appUser is an injected global var in the HTML head (a hacky fix until I get session/cookie stuff working properly)
app.factory('UserService', function($http) {
var pre;
var sessionStatus;
function init() { // Logged in : Logged out
pre = appUser.user != undefined ? appUser.user : { name: 'Logged out', uid: '0' };
sessionStatus = pre.uid != "0" ? true : false;
}
function resetSession() { appUser = null; init(); }
init();
return {
sessionStatus: function() { return sessionStatus; }, // update on change!
logout: function() {
$http.get("/logout").then(function (data, status, headers, config) {
resetSession();
})
}
};
});
Instead of a watch, simply use a scoped function that returns the session status from the service.
$scope.sessionStatus = function() {
return userService.sessionStatus();
};
Your Logout link would look as below:
<a ng-show="sessionStatus()" ng-click="logout()">Logout</a>
A stripped down Plunker for your functionality: http://plnkr.co/edit/u9mjQvdsvuSYTKMEfUwR?p=preview
Using a scoped function is cleaner and is the "correct" way to do it. Yet, for the sake of completeness you could also have fixed your watch:
$scope.$watch(function () {
return UserService.sessionStatus;
}, function() {
$scope.sessionStatus = UserService.sessionStatus;
});
The first argument of the $watch method takes a WatchExpression which can be a string or a method.
But again, $watch should not be used in controllers. Using scoped methods as suggested are cleaner and easier to test.
I'm using the Hot Towel angular library (https://github.com/johnpapa/hottowel-angular-bower) on a project I inherited from a senior developer.
I'm also incorporating Auth0's authentication library for Angular.
I need to restrict some routes to authenticated users. To do that, I set some route properties.
isLogin: true for routes which is restricted to non-authenticated users.
requiresLogin: true for routes needing authentication, and the opposite for those who don't. In order to check these properties on each runthrough, I use $rootScope.$on('$routeChangeStart' function()).
app.run(function ($rootScope, $location, auth, common, config) {
var getLogFn = common.logger.getLogFn,
log = getLogFn('auth handle'),
events = config.events;
$rootScope.$on('$routeChangeSuccess', function (e, nextRoute, currentRoute) {
if (nextRoute.$$route && nextRoute.$$route.settings && nextRoute.$$route.settings.requiresLogin) {
if (!auth.isAuthenticated) {
$location.path('/login');
log('User not authenticated');
}
}
if (nextRoute.$$route && nextRoute.$$route.settings && nextRoute.$$route.settings.isLogin) {
if (auth.isAuthenticated) {
$location.path('/');
log('User is authenticated');
}
}
})
});
Now, it seems this is interfering with the spinner functionality included with Hot-Towel. In Shell.js I find the following:
$rootScope.$on('$routeChangeStart',
function (event, next, current) { toggleSpinner(true); }
);
$rootScope.$on(events.controllerActivateSuccess,
function (data) { toggleSpinner(false); }
);
$rootScope.$on(events.spinnerToggle,
function (data) { toggleSpinner(data.show); }
);
What happens is that the spinner never stops spinning (e.g. vm.isBusy = true because a controller is never activated and resetting this), how would I work around this?
One idea is that you could use an event ($broadcast) to notify when the un authorized access is made so it is then received by the controller that turns off the spinner.
I haven't studied your code for very long but shouldn't you use $routeChangeSuccess to stop the spinner?
$scope.isViewLoading = false;
$scope.$on('$routeChangeStart', function() {
$scope.isViewLoading = true;
});
$scope.$on('$routeChangeSuccess', function() {
$scope.isViewLoading = false;
});