Some time ago, I started to refactor my code of the main project, decoupling the business logic from controllers to services, according to guidelines. Everything went well, until I faced the problem of circular dependency (CD). I read some resources about this problem:
Question 1 on Stack Overflow
Question 2 on Stack Overflow
Miško Hevery blog
Unfortunately, for me it is not clear now, how can I solve the problem of CD for my project. Therefore, I prepared a small demo, which represents the core features of my project:
Link to Plunker
Link to GitHub
Short description of components:
gridCtrl Does not contain any business logic, only triggers dataload and when the data is ready, show grid.
gridMainService This is the main service, which contains object gridOptions. This object is a core object of grid, contains some api, initialized by framework, row data, columns headers and so on. From this service, I was planning to control all related grid stuff. Function loadGridOptions is triggerd by gridCtrl and waits until the row data and column definitions are loaded by corresponding services. Then, it initializes with this data gridOptions object.
gridConfigService This is simple service, which used to load column definitions. No problem here.
gridDataService This service used to load row data with function loadRowData. Another feature of this service: it simulates the live updates, coming from server ($interval function). Problem 1 here!
gridSettingsService This service is used to apply some settings to the grid(for example, how the columns should be sorted). Problem 2 here!
Problem 1:
I have a circular dependency between gridMainService and gridDataService. First gridMainService use gridDataService to load row data with function:
self.loadRowData = function () {
// implementation here
}
But then, gridDataService receives the updates from server and has to insert some rows into grid. So, it has to use gridMainService:
$interval(function () {
var rowToAdd = {
make: "VW " + index,
model: "Golf " + index,
price: 10000 * index
};
var newItems = [rowToAdd];
// here I need to get access to the gridMainService
var gridMainService = $injector.get('gridMainService');
gridMainService.gridOptions.api.addItems(newItems);
index++;
}, 500, 10);
So, here I faced first CD.
Problem 2:
I have a circular dependency between gridMainService and gridSettingsService. First, gridMainService triggering, when the grid is initially loaded and then it sets up default settings by the following function:
self.onGridReady = function () {
$log.info("Grid is ready, apply default settings");
gridSettingsService.applyDefaults();
};
But, to make some changes, gridSettingsService need an access to the gridMainService and its object gridOptions in order to apply settings:
self.applyDefaults = function() {
var sort = [
{colId: 'make', sort: 'asc'}
];
var gridMainService = $injector.get('gridMainService');
gridMainService.gridOptions.api.setSortModel(sort);
};
Question:
How can I solve these CD cases in proper way? Because, Miško Hevery Blog was quite short and good, but I have not managed to apply his strategy to my case.
And currently, I don't like an approach of manual injection a lot, because I will have to use it a lot and the code looks a little bit fuzzy.
Please note:
I prepared only a demo of my big project. You can probably advice to put all code in gridDataService and ready. But, I already has a 500 LOC in this service, if I merge all services, it will be a ~1300 LOC nightmare.
In these kind of problems there are many solutions, which depend by the way you are thinking. I prefer thinking each service (or class) has some goal and needs some other service to complete its goal, but its goal is one, clear and small. Let’s see your code by this view.
Problem 1:
GridData: Here lives the data of grid. MainService comes here to get the data that needs, so we inject this to mainService and we use the loadRowData function to get the rowData data as you do, but in $interval you inject mainService inside gridData but gridData doesn’t need mainService to end its goal (get items from server).
I solve this problem using an observer design pattern, (using $rootScope). That means that I get notified when the data are arrived and mainService come and get them.
grid-data.service.js :
angular.module("gridApp").service("gridDataService",
["$injector", "$interval", "$timeout", "$rootScope",
function ($injector, $interval, $timeout, $rootScope) {
[…]
$interval(function () {
[..]
self.newItems = [rowToAdd];
// delete this code
// var gridMainService = $injector.get('gridMainService');
// gridMainService.gridOptions.api.addItems(newItems);
// notify the mainService that new data has come!
$rootScope.$broadcast('newGridItemAvailable');
grid-main.service.js:
angular.module("gridApp").service("gridMainService",
["$log", "$q", "gridConfigService", "gridDataService", '$rootScope',
function ($log, $q, gridConfigService, gridDataService, $rootScope) {
[..]
// new GridData data arrive go to GridData to get it!
$rootScope.$on('newGridItemAvailable', function(){
self.gridOptions.api.addItems(gridDataService.getNewItems());
})
[..]
When a real server is used, the most common solution is to use the promises (not observer pattern), like the loadRowData.
Problem 2:
gridSettingsService: This service change the settings of mainService so it needs mainService but mainService doesn’t care about gridSettings, when someone wants to change or learn mainService internal state (data, form) must communicate with mainService interface.
So, I delete grid Settings injection from gridMainService and only give an interface to put a callback function for when Grid is Ready.
grid-main.service.js:
angular.module("gridApp").service("gridMainService",
["$log", "$q", "gridConfigService", "gridDataService", '$rootScope',
function ($log, $q, gridConfigService, gridDataService, $rootScope) {
[…]
// You want a function to run onGridReady, put it here!
self.loadGridOptions = function (onGridReady) {
[..]
self.gridOptions = {
columnDefs: gridConfigService.columnDefs,
rowData: gridDataService.rowData,
enableSorting: true,
onGridReady: onGridReady // put callback here
};
return self.gridOptions;
});
[..]// I delete the onGridReady function , no place for outsiders
// If you want to change my state do it with the my interface
Ag-grid-controller.js:
gridMainService.loadGridOptions(gridSettingsService.applyDefaults).then(function () {
vm.gridOptions = gridMainService.gridOptions;
vm.showGrid = true;
});
here the full code: https://plnkr.co/edit/VRVANCXiyY8FjSfKzPna?p=preview
You can introduce a separate service that exposes specific calls, such as adding items to the grid. Both services will have a dependency to this api service, which allows for the data service to drop its dependency on the main service. This separate service will require your main service to register a callback that should be used when you want to add an item. The data service in turn will be able to make use of this callback.
angular.module("gridApp").service("gridApiService", function () {
var self = this;
var addItemCallbacks = [];
self.insertAddItemCallback = function (callback) {
addItemCallbacks.push(callback);
};
self.execAddItemCallback = function (item) {
addItemCallbacks.forEach(function(callback) {
callback(item);
});
};
});
In the above example, there are two functions made available from the service. The insert function will allow for you to register a callback from your main function, that can be used at a later time. The exec function will allow for your data service to make use of the stored callback, passing in the new item.
First of all, I agree with the other comments that the decoupling seems to be a bit extreme; nonetheless, I'm sure you know more than us how much is needed for you project.
I believe an acceptable solution is to use the pub/sub (observer pattern), in which the gridSettingsService and gridDataService services do not directly update the gridMainService, but instead raise a notification that gridMainService can hook into and use to update itself.
So using the plunker that you provided, the changes I would make would be:
Inject the $rootScope service into gridSettingsService, gridDataService and gridMainService.
From gridSettingsService and gridDataService, stop manually injecting the gridMainServiceand instead just $broadcast the notification with the associated data:
```
// in gridDataService
$rootScope.$broadcast('GridDataItemsReady', newItems);
// in gridSettingsService
$rootScope.$broadcast('SortModelChanged', sort);
```
In gridMainService, listed to the notifications and update using the passed in data:
```
// in gridMainService
$rootScope.$on('GridDataItemsReady', function(event, newItem) {
self.gridOptions.api.addItems(newItem);
});
$rootScope.$on('SortModelChanged', function(event, newSort) {
self.gridOptions.api.setSortModel(newSort);
});
```
And here is the updated plunker: https://plnkr.co/edit/pl8NBxU5gdqU8SupnMgy
Related
My angular app have 2 controllers. My problem is that the controllers does not keep the data when the user navigates away from the page.
How can I store the selected data on of my controllers into a data store so it can be used between other controllers?
Option 1 - custom service
You can utilize a dedicated angular service to store and share data between controllers (services are single instance objects)
service definition
app.service('commonService', function ($http) {
var info;
return {
getInfo: getInfo,
setInfo: setInfo
};
// .................
function getInfo() {
return info;
}
function setInfo(value) {
info = value;
}
});
usage in multiple controllers
app.controller("HomeController", function ($scope, commonService) {
$scope.setInfo = function(value){
commonService.setInfo(value);
};
});
app.controller("MyController", function ($scope, commonService) {
$scope.info = commonService.getInfo();
});
Option 2 - html5 localStorage
You can use the built-in browser local storage and store your data from anywhere
writing
$window.localStorage['my-data'] = 'hello world';
reading
var data = $window.localStorage['my-data']
// ...
check out this awesome project:
https://github.com/grevory/angular-local-storage
Option 3 - via web server api
If you need to persist data among different users, you should save it somewhere in the server side (db / cache)
function getProfile() {
return $http.get(commonService.baseApi + '/api/profile/');
}
function updateProfile(data) {
var json = angular.toJson(data);
return $http.post(commonService.baseApi + '/api/profile/', json);
}
EDIT See Jossef Harush's answer where he has written an in-depth response that covers other methods including this one.
I'd recommend using either localStorage or sessionStorage - http://www.w3schools.com/html/html5_webstorage.asp.
HTML local storage provides two objects for storing data on the client:
window.localStorage - stores data with no expiration date
window.sessionStorage - stores data for one session (data is lost when the browser tab is closed)
This assumes that you don't want to POST/PUT the data to your web service (windows service mention in your question).
If you data is an array or some sort, you can convert it to JSON to store as a string and then when you need it you can parse it back as follows - How do I store an array in localStorage?:
var names = [];
names[0] = prompt("New member name?");
localStorage["names"] = JSON.stringify(names);
//...
var storedNames = JSON.parse(localStorage["names"]);
There is an option not mentioned in other answers (AFAIK).
EVENTS
You can use events for communication between controllers.
It's a straightforward communication that doesn't need a mediator
(like service) and can't be wiped by the user (like HTML storage).
All the code is written in controllers that you are trying to
communicate with and thus very transparent.
A good example how to leverage events to communicate between controllers can be seen below.
The publisher is the scope that wanna publish (in other words let others know something happened). Most don't care about what has happened and are not part of this story.
The subscriber is the one that cares that certain event has been published (in other words when it gets notified hey, this happened, it reacts).
We will use $rootScope as a mediator between publisher and a subscriber. This always works because whatever scope emits an event, $rootScope is a parent of that scope or parent of a parent of a parent.. When $rootScope broadcasts (tells everyone who inherits) about an event, everyone hears (since $rootScope is just that, the root of the scope inheritance tree) so every other scope in app is a child of it or child of a child of a child..
// publisher
angular.module('test', []).controller('CtrlPublish', ['$rootScope','$scope',
function ($rootScope, $scope) {
$scope.send = function() {
$rootScope.$broadcast('eventName', 'message');
};
}]);
// subscriber
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {
$scope.$on('eventName', function (event, arg) {
$scope.receiver = 'got your ' + arg;
});
}]);
Above we see two controllers communicating a message to each other using an event. The event has a name, it has to be unique, otherwise, a subscriber doesn't differentiate between events. The event parameter holds autogenerated but sometimes useful data, the message is the payload. In this example, it's a string but it can be any object. So simply put all the data you wish to communicate inside an object and send it via event.
NOTE:
You can avoid using root scope for this purpose (and limit the number of controllers that get notified of an event) in case two scopes are in direct inheritance line of each other. Further explanation below:
$rootScope.$emit only lets other $rootScope listeners catch it. This is good when you don't want every $scope to get it. Mostly a high level communication. Think of it as adults talking to each other in a room so the kids can't hear them.
$rootScope.$broadcast is a method that lets pretty much everything hear it. This would be the equivalent of parents yelling that dinner is ready so everyone in the house hears it.
$scope.$emit is when you want that $scope and all its parents and $rootScope to hear the event. This is a child whining to their parents at home (but not at a grocery store where other kids can hear). This is a shortcut to use when you wanna communicate from the publisher that is a child or n-th child of the subscriber.
$scope.$broadcast is for the $scope itself and its children. This is a child whispering to its stuffed animals so their parents can't hear.
EDIT: I thought plunker with a more elaborate example would be enough so I decided to keep is simple here. This elaborate explanation should be better.
To share data between two controllers on the same page, you can use factories/services. Take a look at Share data between AngularJS controllers for example.
However, if this is across page reloads/refreshes, you will need to store the data in local storage and then read it upon reloading. An example of that is here: How do I store data in local storage using Angularjs?
Checkout this library https://www.npmjs.com/package/angularjs-store
This can help you manage your application state much simpler as it will force you to have a one way data flow on your application.
This is code from an Angular introduction video series, which explains how to populate angular controllers with data from persisted memory, but stops just short of explaining how to add the new product reviews to the persisted memory.
There do seem to be some articles explaining how to do this, but since I am very new to angular, I'm afraid I couldn't understand any of them.
I have figured out the syntax for making post requests using $http, but I don't see how to fit that code into the existing structure, so that it will 1) be called when pushing a new element to the reviews array, and 2) update the view when completed.
I am interested to learn a basic way to add the new product review to persistent memory.
(function() {
var app = angular.module('gemStore', ['store-directives']);
app.controller('StoreController', ['$http', function($http){
var store = this;
store.products = [];
$http.get('/store-products.json').success(function(data){
store.products = data;
});
}]);
app.controller('ReviewController', function() {
this.review = {};
this.addReview = function(product) {
product.reviews.push(this.review);
this.review = {};
};
});
})();
The JSON looks like this:
[
{
"name": "Azurite",
"description": "...",
...
"reviews": []
},
...
]
If the store-products.json is just a file on the server, you'll need an actual backend implementation (in PHP, nodejs, etc.) to actually update the file (or more typically just return the content from the database).
Normally you would make a save method and not post on every modification, though. But, in either case, depending on your backend, usually the implementation is as simple as $http.put('/store-products', store.products) whenever you click a "save" button. Typically, the put can return the same data, so there's typically no need to update the view since you just set it exactly to your state. But, if you have possibility of concurrent editing, and the put returns the modified data, it would look like your get:
$http.put('/store-products', store.products).success(function(data){
store.products = data;
});
For adding an item, it might almost identical, depending on your data model:
$http.post('/store-products', newProduct).success(function(data){
store.products = data;
});
In this case the POST gives an item to add and returns all of the products. If there are a lot of products -- that is, products are more like a large database than a small set in a "document", then the post would more typically return the added item after any server processing:
$http.post('/store-products', newProduct).success(function(newProductFromServer){
store.products.push(newProductFromServer); //if newProduct wasn't already in the array
//or, store.products[newProductIdx] = newProductFromServer
});
If you really wanted to call this function on every modification instead of a save button, you can use a watch:
$scope.$watchCollection(
function() { return store.products; },
function() { /* call the $http.put or post here */ }
}
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 :/
I'm trying to split up and organise an AngularJS application so that it isn't just a 5000 line main.js file. Splitting off directives, etc. and using make to build working code is all fine. However, my controller has a couple of moderately complex internal classes. These used to be defined roughly as follows (only one shown for clarity):
var app = angular.module("infrasense", []);
app.controller("AppMain", function($scope, $http, $timeout) {
function NavTree(dbMain, dbTimeout, allTagTypes, allAttTypes) {
...
}
NavTree.prototype = {
...
}
...
$scope.navTree[0] = new NavTree(dbMain, dbTimeout);
...
});
The NavTree class (which holds a hierarchical tree of sites and assets in a data logging application) is rendered using a directive and uses $http internally to talk to a backend server (the tree is too complex to be held in memory at once, plus it changes).
In order to keep using a simple (cat-based) tool to generate my final code I want to move NavTree out of the controller. I currently do this by passing $http into it from inside the controller:
function NavTree($http, dbMain, dbTimeout, allTagTypes, allAttTypes) {
...
this.$http = $http;
...
}
app.controller("AppMain", function($scope, $http, $timeout) {
...
$scope.navTree[0] = new NavTree($http, dbMain, dbTimeout);
...
});
This works but feels inelegant and non-AngularJS-ish. Can anyone suggest the "proper" way to do this sort of thing?
Success! I am now a good proportion of the way towards moving these inelegant internal classes out of main.js and into services, where they belong.
The key realisation, which I'd missed when reading the documentation but which was restated in "I Wish I Knew Then What I Know Now — Life With AngularJS" is that services are just singletons which work with dependency injection.
My service is defined as follows (where "Popup" is another service that manages popup windows for error messages):
app.factory("ThingTree", function (Popup, $q, $http) {
// Database information. This is set up by the "init" function.
// (There's only one DB, and this way I only have to pass its
// connection info once.)
var dbMain = "";
var dbTimeout = 0;
...
// Each level of the tree is an array of these tree objects.
function TreeNode() {}
TreeNode.prototype = {
open: function() { ... }
...
};
return {
// Initialise the database connection.
init: function(myDbMain, myDbTimeout) {
dbMain = myDbMain;
dbTimeout = myDbTimeout;
...
},
// Create a tree and return the root node.
create: function() { ... },
...
}
});
Thanks for the prods in the right direction!
I have my angular controller setup like most of the examples shown in the docs such that it is a global function. I assume that the controller class is being called when the angular engine sees the controller tag in the html.
My issue is that i want to pass in a parameter to my controller and i don't know how to do that because I'm not initializing it. I see some answers suggesting the use of ng-init. But my parameter is not a trivial string - it is a complex object that is being loaded by another (non-angular) part of my js. It is also not available right on load but takes a while to come along.
So i need a way to pass this object, when it finally finishes loading, into the controller (or scope) so that the controller can interact with it.
Is this possible?
You can use a service or a factory for this, combined with promises:
You can setup a factory that returns a promise, and create a global function (accessible from 3rd-party JS) to resolve the promise.
Note the $rootScope.$apply() call. Angular won't call the then function of a promise until an $apply cycle. See the $q docs.
app.factory('fromoutside', function($window, $q, $rootScope) {
var deferred = $q.defer();
$window.injectIntoAngularWorld = function(obj) {
deferred.resolve(obj);
$rootScope.$apply();
};
return deferred.promise;
});
And then in your controller, you can ask for the fromoutside service and bind to the data when it arrives:
app.controller('main', function($scope, fromoutside) {
fromoutside.then(function(obj) {
$scope.data = obj;
});
});
And then somewhere outside of Angular:
setTimeout(function() {
window.injectIntoAngularWorld({
A: 1,
B: 2,
C: 3
});
}, 2000);
Here's a fiddle of this.
Personally, I feel this is a little bit cleaner than reaching into an Angular controller via the DOM.
EDIT: Another approach
Mark Rajcok asked in a comment if this could be modified to allow getting data more than once.
Now, getting data more than once could mean incremental updates, or changing the object itself, or other things. But the main things that need to happen are getting the data into the Angular world and then getting the right angular scopes to run their $digests.
In this fiddle, I've shown one way, when you might just be getting updates to an Array from outside of angular.
It uses a similar trick as the promise example above.
Here's the main factory:
app.factory('data', function($window) {
var items = [];
var scopes = [];
$window.addItem = function(item) {
items.push(item);
angular.forEach(scopes, function(scope) {
scope.$digest();
});
};
return {
items: items,
register: function(scope) { scopes.push(scope); }
};
Like the previous example, we attach a function to the $window service (exposing it globally). The new bit is exposing a register function, which controllers that want updates to data should use to register themselves.
When the external JS calls into angular, we just loop over all the registered scopes and run a digest on each to make sure they're updated.
In your non-angular JavaScript, you can get access to the scope associated with a DOM element as follows:
angular.element(someDomElement).scope().someControllerFunction(delayedData);
I assume you can find someDomElement with a jQuery selector or something.