I have an app that has items, and you can do things like add new items, update the text of an item, move the item to a different folder, etc.
I have an items factory that holds all the items as plain objects inside an array, and the factory returns a singleton that has various methods, like get(), set(), etc.
To add some context to the question, I'm working with Node.js and MongoDB as well.
Anyway, due to all the various factories I have, like items, folders, and all the various controllers for different views, I am relying heavily on events. To give some examples:
// items factory
update: function(params) {
// add to database, then...
.then(function() {
$rootScope.$emit('itemCreated');
});
}
// items controller
// I need to refresh the items list in the scope
$rootScope.$on('itemCreated', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
These are their own kind of "subset" of events, in that they all pertain to "CRUD" operations on items.
But, I also have other events that I use. For example, I have an interceptor that listens to any requests and responses. I have a loading widget (an image of a spinning wheel) that uses a directive. This directive will show the loading widget when a request begins, and hide the loading widget when a request ends. This is also event based.
// on request
$rootScope.$emit(_START_REQUEST_);
// on any response
$rootScope.$emit(_END_REQUEST_);
I attempted to "modularize" these request and response events by simply making them constants.
.constant('_START_REQUEST_', '_START_REQUEST_');
I am trying to find a solution in order to "modularize" all my other events, like the events emitted on CRUD operations for items. One idea I've had is to define all of the item CRUD events inside the items factory:
events: {
update: 'itemUpdate',
create: 'itemCreated'
// etc.
}
Then, I can simply inject my items factory into a controller, and reference events like so:
$rootScope.$on(items.events.update, function() {});
I also considered simply defining all events, regardless of whether they are interceptor events or item events, as constants in my app. However, it seemed like this solution directly coupled item events to the module itself, rather than to the items factory, which is where I feel they "belong".
Basically, the issue is that right now all my events definitions seem to be scattered around. My question is: what pattern or best practice would you recommend for modularizing and defining events in AngularJS?
I agree that these item events should belong to the event source. You could implement a observer pattern in the item factory that hides the dependency on $rootScope for event listeners. This way the event key itself is a private detail of the item factory, and the subscription to the event is made explicit by calling a dedicated function for it. This approach makes your code more independent of $rootScope and easier to maintain than an event name convention (thinking about usages search for the specific event subscription method vs. usages of $rootScope.$emit / $on):
angular.module('events', [])
.service('items', ['$rootScope', function($rootScope) {
var createdEventKey = 'item.created';
return {
create: function () {
$rootScope.$emit(createdEventKey, {"name": "aItemName"});
},
onCreated: function(callback, scope) {
var unsubscribeFunction = $rootScope.$on(createdEventKey, function(event, payload) {
callback(payload);
});
// allow to unsubscribe automatically on scope destroy to prevent memory leaks
if (scope) {
scope.$on("$destroy", unsubscribeFunction);
}
return unsubscribeFunction;
}
}
}])
.controller('TestController', function($scope, items) {
items.onCreated(function (item) {
console.log("Created: " + item.name);
}, $scope);
});
complete example: http://jsfiddle.net/8LtyB/32/
If all you want is a way to create a separate object for containing the names of events, why not use a service?
myApp.service('itemEvents', function () {
var events = {
update: 'itemupdate',
create: 'itemcreate',
...
};
return events;
});
This is essentially what you had before when you were suggesting using a factory to contain the event definitions, except that a service is a single object instance, and is instantiated at module start-up. In contrast, a factory creates a new instance when injected into a controller. (Here's a good SO post on the difference between services and factories)
You can inject this service into your controllers or directives:
myApp.controller('ItemController', function ($scope, itemEvents) {
$scope.on(itemEvents.update, function () { /* something interesting */ });
});
This gives you a nice place to centralize your event name definitions. As a side note, some people hold to the convention of using all lowercase when defining event names (so itemupdate instead of itemUpdate). Hope this helps!
You can use the following:
app.config(function($provide) {
$provide.decorator("$rootScope", function($delegate) {
var Scope = $delegate.constructor;
var origBroadcast = Scope.prototype.$broadcast;
var origEmit = Scope.prototype.$emit;
Scope.prototype.$broadcast = function() {
console.log("$broadcast was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origBroadcast.apply(this, arguments);
};
Scope.prototype.$emit = function() {
console.log("$emit was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origEmit.apply(this, arguments);
};
return $delegate;
});
})
example: http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview
src: https://github.com/angular/angular.js/issues/6043
assuming these $scope.$emit works like jquery events I would suggest you name your emits to be generic for example in you database update simply do this:
$rootScope.$emit('Created')
then in your items controller do this :
$rootScope.$on('Created.item', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
then you can wire to the created event in any of your controllers and its name is generic. The .item should add a namespace. if you make all of your events in your items controller have the .item name space you should be able to do a
$rootScope.$off('item')
This will clear up memory leaks
Related
I am using angular and grafana in my project.
I have a service -> dashboardViewStateSrv
My Service Code :
define([
'angular',
'lodash',
'jquery',
],
function (angular, _, $) {
'use strict';
var module = angular.module('grafana.services');
module.factory('dashboardViewStateSrv', function($location, $timeout) {
function DashboardViewState($scope) {
var self = this;
self.state = {};
self.panelScopes = [];
self.$scope = $scope;
// something
}
return {
create: function($scope) {
return new DashboardViewState($scope);
}
};
});
});
In My side menu controller :
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
if ($scope.dashboardViewState) {
if($scope.dashboardViewState.state.citreedepth){
depth = +$scope.dashboardViewState.state.citreedepth;
}
}
In My Dashboard controller :
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
DashboardViewState object is being created twice (Dashboard Ctrl and Side Menu ctrl).
I am creating DashboardViewState object twice, I want to avoid that. If I can avoid creating DashboardViewState object in Side Menu ctrl?
There should be only one view state. As per my understanding all the services are singleton in angular.
Please guide me what I can do?
Services are singletons, they are essentially a constructure function allowing you to use the this keyword inside them. They are instantiated once when first created then that instance is shared throughout your app.
Factories are, well, factories. Somewhere in Angular it will call Object.create() on the object your return from a factory. Meaning each call will return a new instance of it.
So in your use case your creating a new object twice. First by using a factory, then second by returning a new object from that factory.
This may help http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html
So if you want a single instance of an object through your application you should use .service() not .factory();
If you want to instantiate a new Object only once you could use a service. Have the object as a property and a get method. The service could check if the object is already created and if not make it.
something like this (example code, not tested):
module.service('dashboardViewStateSrv', function($location, $timeout) {
this.object;
this.get = function (){
if(this.object === undefined) {
return this.object = Object.create({}); //Create your object
} else {
return this.object;
}
}
});
however i did notice some booboo's (Sorry always reminds me of Hook when i say that). First you do not need to alias the this keyword, were not working in an jQuery callback, even if we were you can bind your function etc.
Second and this is important. Your passing a $scope object into your service, this is very very bad. Not just for this reason but how can controllers share a single service object if it has a reference to a $scope? Your services should be a collection of single simple methods that have their input and output data. They work on that and continue. You can then chain them and pass them the specific data each method needs. They shouldn't be a monolithic object that has everything in hidden properties, think functional, a pipeline if you will.
For the sake of understanding suppose I have an AngularJS application that has similar data as Stackoverflow so that it:
is using the usual ngRoute/$routeProvider
has a userService that returns favourite and ignore tag lists of the logged in user - both lists are fetched at the same time and request for them is a promise that when resolved caches these lists
has a view that displays a list of questions with a QuestionsController that provides its model (similar to Stackoverflow)
QuestionsController makes a request for questions and then depends on cached tag lists to mark them appropriately
As the recommended guideline when controllers rely on other async data we should offload those to route resolve part so when controllers are being instantiated those promises are already resolved. Therefore I offload tag list fetching to it so both lists are ready and injected into the controller. This all works as expected.
The additional feature of my questions list view is that when a user clicks a tag displayed on questions it automatically adds this tag to favourite list (or off of it when that tag is already part of favourite list).
Route configuration
...
.when({
templateUrl: "...",
controller: "QuestionsController as context",
resolve: {
tags: ["userService", function(userService) {
return userService.getMyTags();
}]
}
})
.when(...)
...
Controller pseudo code
QuestionsController.prototype.markQuestions = function() {
this.model.questions.forEach(function(q, idx) {
// "myTags" is resolve injected dependency
q.isFavourite = q.tags.any(myTags.favourite);
q.isIgnored = q.tags.any(myTags.ignored);
});
};
QuestionsController.prototype.toggleTag = function(tag) {
var self = this;
// change tag subscription
tagService
.toggleFavourite(tag)
.then(function() {
// re-mark questions based on the new set of tags
self.markQuestions();
});
};
The problem
When the view displays, all questions are loaded and correctly marked as per provided tag lists. Now when a user clicks on a specific tag and that tag's favourite status gets changes my controller's dependency should get automatically updated.
How can I do that since my controller is already instantiated and had tag lists injected during instantiation?
I would like to avoid loading those lists manually within my controller because in that case I should do the same during instantiation and reuse the same functionality and not have it in two places (route resolve and inside controller).
So long as your "resolved" variable is referring to the same object used elsewhere, they are one and the same.
So, if your userService.getMyTags is conceptually like the following:
.factory("userService", function($timeout){
var tags = [/*...*/];
return {
getMyTags: function(){
return $timeout(function(){ return tags; }, 500);
},
addTag: function(newTag){
tags.push(newTag);
}
}
});
Then any reference to tags anywhere would get the changes:
.controller("ViewCtrl", function($scope, tags){
$scope.tags = tags; // tags is "resolved" with userService.getMyTags()
})
.controller("AddTagCtrl", function($scope, userService){
$scope.addTag = function(newTag){
userService.addTag(newTag); // changes will be reflected in ViewCtrl
}
}
plunker, to illustrate
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
How can I switch out a service on-the-fly and have all components (relying on the service) automatically be bound to the data on the new strategy?
I have a Storage service and two storage strategies, StorageStrategyA and StorageStrategyB. Storage provides the public interface to controllers and other components to interact with:
angular.module('app').factory('Storage', function ($injector) {
var storage;
var setStrategy = function (name) {
storage = $injector.get(name);
};
setStrategy('StorageStrategyB');
return {
getItems: function () {
return storage.getItems();
}
// [...]
};
});
But when the strategy is changed, the two-way binding breaks and the view doesn't update with items from getItems() from the new strategy.
I've created a Plunker to illustrate the problem.
Is there a way to combine the strategy pattern with AngularJS and keep the two-way binding?
Please note that in my actual app I cannot just call Storage.getItems() again after the strategy has been changed, because there are multiple components (views, controllers, scopes) relying on Storage and the service change happens automatically.
Edit:
I have forked the Plunker to highlight the problem. As you can see, the data in the upper part only updates because I manually call Storage.getItems() again after the strategy has been changed. But I cannot do that, because other component - for example OtherController - are also accessing data on Storage and also need to automatically get their data from the new strategy. Instead, they stay bound to the initial strategy.
Javascript works on references. Your array items in app is same reference as items of strategyB items initially with the below statement and when you update the StrategyB items automatically items in your view gets updated(since same reference).
$scope.items = Storage.getItems();
So, when you switch strategy you are not changing the reference of items. It still points to StrategyB items reference.
You have to use the below mechanism to change the reference.
Then you can do something where you can communicate between controllers to change the items reference.
Please find the plunkr I have updated.
$rootScope.$broadcast("updateStrategy");
And then update your item list and others.
$scope.$on("updateStrategy",function(){
$scope.name = Storage.getName();
$scope.items = Storage.getItems(); //Here changing the reference.
//Anything else to update
});
the two way binding is still ok, you have a reference issue.
when the AppController set up the $scope.items set to the StorageStrategyB items, then when you switch to StorageStrategyA, the AppController $scope.items is still set to StorageStrategyB items.
angular.module('app').controller('AppController', function ($scope, Storage) {
Storage.setStrategy('StorageStrategyB');
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
$scope.setStrategy = function (name) {
Storage.setStrategy(name);
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
console.log( $scope.items);
console.log($scope.current);
};
$scope.addItem = function () {
Storage.addItem($scope.item);
$scope.item = '';
};
});
You forgot
$scope.items = Storage.getItems();
nice question :)
plnkr
I have a number of backbone models, organized in collections and connected to their corresponding views and/or collections of views. Some of these models that do not belong to the same collection need to trigger an event which is of interest to another model (and maybe more than one).
The recommended way to deal with this is, I think, the "global event dispatcher/aggregator" as described here and other places.
However, I also happen to be using require.js, which seems to go against the idea of attaching the dispatcher/aggregator to the application's namespace object -- or am I wrong here?
So my question is: using require.js how can I have a number of different backbone models trigger an event that will be handled by another model?
A similar solution to what #Andreas proposed but without Backbone.Marionette (heavily inspired nonetheless, see the article linked in the question).
All you have to do is to define a module that creates a singleton of an event listener and require this object in the modules where you want to trigger an event or listen to this event.
Let's say you have app/channel.js defining your channel
define(['backbone', 'underscore'], function (Backbone, _) {
var channel = _.extend({}, Backbone.Events);
return channel;
});
You can then use it as a listener via
require(['app/channel'], function (channel) {
channel.on('app.event', function () {
console.log('app.event');
});
});
and you can trigger an event on this channel via
require(['app/channel'], function (channel) {
channel.trigger('app.event');
});
We using Marionettes app.vent (which is the global event transmitter for our application), allong with require js and it works really well.
app
define(, function(){
return new Backbone.Marionette.Application();
})
Model1
define(['app'], function(app){
return Backbone.Marionette.Model.extend({
initialize: function(){
this.bindTo('app.vent', 'create:model2', this.toSomething, this);
}
})
})
Model2
define(['app'], function(app){
return Backbone.Marionette.Model.extend({
initialize: function(){
app.vent.trigger('create:model2', this);
}
})
})