I have implemented a single page application with AngularJS. The page consists of a content area in the middle and sections assembled around the center that show additional info and provide means to manipulate the center.
Each section (called Side Info) and the content area have a separate AngularJS controller assigned to them. Currently, I communicate via $rootScope.$broadcast and $scope.$on(), e.g.
app.controller('PropertiesController', function ($scope, $rootScope) {
$scope.$on('somethingHappened', function(event, data){
// react
});
});
I then call to communicate with other controllers:
$rootScope.$broadcast('somethingHappened', data);
I have quite a lot of communication happening between the Controllers. Especially if something is going on in the content area, several side info elements have to adopt. The other way around is also frequent: a user submits a form (located in a side info) and the content area and other side info elements have to adopt.
My question:
Is there a better way to handle SPA with heavy controller communication?
The code works fine but it is already getting a bit messy (e.g. it is hard to find which events are handled where etc.). Since the application is likely to grow a lot in the next weeks, I'd like to make those changes (if there are any better solutions) asap.
This is really interesting. Pub/Sub should be a right solution here.
You could add extra order to your project by using Angular services as your MVC's model, and update this model for each change. The issue here is that you should implement an observable pattern inside your service and register to them, in order for this to be live synced. So - we're back to Pub/Sub (or other Observable solution that you could think about...).
But, the project will be better organised that way.
For example - SideInfo1Service will be a service/model. Each property change will trigger an observable change which will change all listeners:
myApp.factory('SideInfo1Service', function($scope){
var _prop1;
return {
setProp1: function(value){
$scope.$broadcast('prop1Changed', value);
_prop1 = value;
},
getProp1: function(){
return _prop1;
}
}
});
You could find those really interesting blog posts about using Angular Services as your MVC's model:
http://toddmotto.com/rethinking-angular-js-controllers/
http://jonathancreamer.com/the-state-of-angularjs-controllers/
And, this post is about observable pattern in Angularjs:
https://stackoverflow.com/a/25613550/916450
Hope this could be helpful (:
You have multiple options in order to avoid broadcasts calls:
Share data between controllers using services like it was mentioned in the comments. You can see how to this at: https://thinkster.io/egghead/sharing-data-between-controllers
Create a main controller for the whole page and child controllers for each section (Content Area and Side Info). Use scope prototype inheritance. For example:
if in main controller you have:
$scope.myObject = someValue;
in child Controllers you can set:
$scope.myObject.myProperty = someOtherValue;
you can access myObject.myProperty from your Main Controller
You can use
$rootScope.$emit('some:event') ;
because it goes upwards and rootscope ist the top level
use
var myListener = $rootScope.$on('some:event', function (event, data) { });
$scope.$on('$destroy', myListener);
to catch the event
Then you have a communication on the same level the rootscope without bubbling
Here is my implemented eventbus service
http://jsfiddle.net/navqtaoj/2/
Edit: you can use a namespace like some:event to group and organize your event names better and add log outputs when the event is fired and when the event is catch so that you easy can figure out if fireing or catching the wrong eventname.
Very important question and very good answers.
I got inspired and created three plunks showing each technique:
Broadcasting: http://embed.plnkr.co/lwSNDCsw4gjLHXDhUs2R/preview
Sharing Service: http://embed.plnkr.co/GptJf2cchAYmoOb2wjRx/preview
Nested Scopes: http://embed.plnkr.co/Bct0Qwz9EziQkHemYACk/preview
Check out the plunks, hope this helps.
Related
my MusicPlayer.js Angular service has a callback function wrapped in $rootScope.$apply that updates a specific object (musicPlayer.currentPlaybackTime) and is shared to all other controllers ( via applying to $rootScope).
I understand that you'll ideally want to limit any $rootScope pollution, so i'm looking at possible refactoring options that take away calling apply methods to $rootScope but allows my updated object to be shared across multiple controllers.
My research indicates that i'll need to register the other controllers (i.e. PlayerDashboardCtrl.js, PlaylistCtrl.js and AlbumListCtrl.js) that need my currentPlaybackTime object, but i'd like to understand what's the most efficient way of doing this.
Thank you.
var setSong = function(song) {
currentBuzzObject = new buzz.sound(song.audioUrl, {
formats: ['mp3'],
preload: true
});
currentBuzzObject.bind('timeupdate', function() {
$rootScope.$apply(function() {
musicPlayer.currentPlaybackTime = currentBuzzObject.getTime();
});
});
musicPlayer.currentSong = song;
};
The best way to share data between controllers is to make a service/factory and get data using these service from whichever controller you want. You will have to inject this service in all the controllers where you want to access them.
This egghead video will give you a clear understanding: Share data between controllers
Stackoverflow question similar to this: Stackoverflow answers to sharing data between controllers.
Live Demo: Fiddle to show data sharing.
I have an AngularJS site, the object-resource I want to show is:
each user has a basic account, that will show in a single page (named basic-page);
user has several sub-account, each sub-account will show in a diffent page (named app-page);
basic-page will show the summer info about the sub-account, so app-page can share the loaded $http data of basic-page is better for code reusing.
As the purpose, I use ui-router define state below:
.state('user', {
url: '/user/{id}',
title: 'User-Page',
templateUrl: helper.basepath('user.html')
})
.state('user.app', {
url: '/{app}',
title: 'App-Page',
emplateUrl: helper.basepath('app.html')
})
Notice that state user.app is the child of user.
What I want is when I enter the user.app, it can reuse the data in user, ecen if it's a different page, that the user need not to contain a ui-view to include user.app's template.
But actually I enter user.app, and it doesn't show the app.html(because I didn't include ui-view in user.html).
Maybe this is not the correct usage of ui-router.
So, how can I share data in different $state? Anyone can give me a detailed example? Thank you.
Sharing data across controllers
Any time you need to share data across states you will need to create a service/factory that you can instantiate in your controllers associated with those states.
The factory will consist of basic getters and setter for the different data you need to share. Just like when you build getters and setters in java to share across classes.
Example Code
.factory('yourFactory', function ($scope) {
return {
get: function () {
return $scope.someValue;
},
set: function(value){
$scope.someValue = value;
}
};
})
Disclaimer: I've not tested this code but it should do the job for getting and setting some values you need to access across your app.
Demo : Working plunker with this approach.
Alternative: 1
This is the "Dirty" alternative, you can set a global variable with $rootScope. It will be accessible everywhere since its global, I strongly advise you don't do this but though I would point it out to you anyway.
Alternative: 2
When a state is "active"—all of its ancestor states are implicitly active as well.So you can build your states considering the parent-child relationship and share data across scopes in hierarchical manner.
Official Docs and working plunker with mentioned approach.
I am trying my hands on the new ExtJs 5.
I have created a small app as per the defined MVC pattern of ExtJs5.
Am using ViewControllers for each View.
Problem Statement: Now suppose I have two VCs (Controller1 & Controller2). Each has its own methods. I wish to call a method of Controller2 from Controller1. I want to update the View associated with the Controller2 from Controller1.
E.g. Suppose there is a separate view for Status Bar and a ViewController(StatusBarController).
This VC has a method to update the view based on whatever message it receives as input parameter.
All the other controllers in the application will call this VCs method to update the status of the application on the status bar.
In the previous versions, this.getController('StatusBarController') was used to get the handle to any controller and then call its method.
But this is not working in my case when I use a ViewController.
Can anyone guide me how to achieve this thing? And also whether it is the correct/ideal way to do such a thing or is there any better option?
Here is my code:
StatusBarView:
Ext.define('MyApp.view.statusbar.StatusBarView', {
extend : 'Ext.panel.Panel',
controller: 'StatusBarController',
region : 'south',
xtype : 'status-bar-panel',
html : 'This is a status bar'
});
StatusBarController:
Ext.define('MyApp.controller.StatusBarController', {
extend : 'Ext.app.ViewController',
alias: 'controller.StatusBarController',
updateStatusBar : function(message) {
this.getStatusBarView().update(message);
}
});
Some Other Controller in app:
Ext.define('MyApp.controller.ResourcesPanelController', {
extend : 'Ext.app.ViewController',
alias : 'controller.ResourcesController',
onItemClick : function(tree, record, item, index, e, eOpts) {
// here I am calling the other controller's method.
this.getController('StatusBarController').updateStatusBar(
record.data.text + ' has been clicked');
}
});
ViewControllers are tightly related to their views, they are even created and destroyed together with views, and they should be controlling only their own views. The idea is to separate logic from UI on the view level.
Calling methods of one ViewController from another is not a good practice and, for big applications, it is route to hell as it inevitably leads to unmaintainable spaghetti code.
The correct approach is minimize the number of ViewModels, ViewControllers and Controllers and let them work in their own areas of responsibilities.
For example: Suppose you want a grid and form in a container. Form would allow editing of the record selected in the grid. Plus some buttons. These three views (container, grid and form) together form a unit. Thus:
only one ViewController at container is needed, all views can use it
only one ViewModel at container is needed, all view can use it
if you want to let this trio to communicate with the outer world of the rest of the application, the container's view controller can fire events and can have API methods to call
Thus, if needed, you can have an MVC (global) Controller(s) that would coordinate functions of units, like our trio.
Also, data binding simplifies the logic to a great degree so controllers and listeners are not needed that much.
See Binding Grid and Form in ExtJS 5 example.
my answer is simple and short:
Ext.app.ViewController.fireEvent()
while one can add any type of custom event with the listeners config of the ViewController - the docs of the listen config state "event domains", so I'd assume, that both controller need to reside within the same domain in order to be able to interact, event-wise.
the 2nd argument of .fireEvent() might need to imitate the element which ordinary triggers the event.
well, it should also be possible to access it like that (in the secondary controller):
this.getApplication().getStatusBarController().updateStatusBar('...');
I am wondering if there is a convention within AngularJS for creating an object that lives within the app module, but is not attached directly to the view in any way, but is called when the view has loaded and the app starts up. In particular, I am trying to write an object that dispatches messages to listening controllers when they come in from the server.
Currently, I have implemented this by creating a "Controller" that attaches to the view. It has a monitor() function that is called when the page loads, and then listens in a loop for any incoming messages. I call the monitor() function from within the loaded view, by setting the ng-controller like so:
<div ng-controller="MyController">
{{ monitor() }}
</div>
This doesn't feel like the right thing to do. This "Controller" isn't interacting with the view in any way, so my gut tells me I am violating principles of AngularJS. But I haven't been able to turn up an easy solution that is endorsed by the AngularJS doc.
I am looking for a way to create an object that lives within the AngularJS world (in other words, it can use dependency injection to get access to services, and it can use $scope.$broadcast to send messages to other listening controllers), but that doesn't need to attach itself to the view in any way.
Ideally, I am looking for a way to say, "Here Angular, on startup, create this object, and run this method on it." Is there a way to do this?
You may use this as a starting point:
declaration of your object.
AngularJS: Service vs provider vs factory
myApp.factory('MessageBus', function() {
return {
listeners: [],
init: function() {
// do whatever you need at startup
},
pushMessage: function(msg) {
angular.forEach(this.listeners, function(listener) {
listener(msg);
});
},
subscribe: function(onMessageCallback) {
this.listeners.push(onMessageCallback);
}
};
});
calling a method on angular appilcation start
https://docs.angularjs.org/api/ng/type/angular.Module#run
myApp.run(function(MessageBus) {
MessageBus.init();
});
using this object within controllers
https://docs.angularjs.org/guide/di
myApp.controller('MessageCtrl', function($scope, MessageBus) {
$scope.messagesToShow = [];
MessageBus.subscribe(function(message) {
$scope.messagesToShow.push(message);
});
$scope.submitMessage = function(id, text) {
MessageBus.pushMessage({
type: 'TEXTMESSAGE',
id: id,
payload: text
});
};
});
Note that this is something to start with and nothing for any production code. For example the controller doesn't unsubscribe after being destroyed - if the page changes - and so you leak memory.
Don't use $broadcast-events for this
1: they are slow
2: if this MessageBus has a specific concern, than in should be an own object with a meaningfull name and api. Otherwise your $rootScope will be flooded with thousends of different events for different concerns when your application grows. A service is always easier to document and you have a clean dependency on that specific service. Only using events on the $rootScope hides this dependency from every developer reading and hopefully understanding your codebase,
Yeah you approach is really smelly. This function will be called every time a $apply/$digest invokes.
Maybe move the function into the run callback on the module.
var app = angular.module("YourApp", [//dependencies]);
app.run(function($YourUIService){
$YourUIService.monitor();
});
The run will be invoked, when your angularjs-module has loaded every dependency and is ready to run.
Didn't find the doc for this :/
Using $routeProvider every time user clicks on a link, a new $scope is being generated. That means all the data is lost. How can i make Angular use the same controller/$scope?
Explanation:
http://jsfiddle.net/mpKBh/1/
(click on links)
<a href='#'>First controller</a>
<a href='#/view'>Second controller</a>
$routeProvider.
when('/', { template:"{{$id}}",controller: ContentListCtrl}).
when('/view', {template:"{{$id}}",controller: ContentDetailCtrl}).
P.s. is it possible to know which controller is currently active?
In AngularJS, $scope is not meant to hold data that persists across your application. For that, you want to use a service that is injected into both controllers. If you provide more detail on what data you're missing across routes, I would be happy to revise this answer to include something a little more actionable.
In re your PS: You can inject the $route service to get information about the current route; the $route.current.controller property will give you the constructor function of the current route.
For those researching how to "unbind" in AngularJS, he is a bit of info (related to the OP's last comment above)
When a view is destroyed, it's basically marked for garbage collection - but it's still there. That is why you are getting multiple requests when a scroll happens - because it's still listening for events.
So the easiest way to deal with this (that I have found, though I'd like to learn of other ways) is to listen for the $destroy event and react on it.
You can "unbind/unlisten" for an event by keeping a reference to what is returned by the $on method. Here is an example taken from a controller:
$scope.systemListener = $rootScope.$on("someEventYouListenTo", function (event, data) {
console.log('Event received by ' + $scope.name);
});
$scope.$on('$destroy', function () {
// Remove the listener
$scope.systemListener();
});
Now those old scopes/views won't react to events anymore.
Hope that helps someone!