Using $http outside of controllers? - javascript

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!

Related

When should I create factory or VM in angular

This is how my angular app looks like.
(function () {
"use strict";
angular
.module("app")
.controller("custCtrl", custCtrl);
custCtrl.$inject = ['dataService','custFactory'];
/* #ngInject */
function custCtrl(dataService, custFactory) {
var vm = this;
//line##
vm.customer= custFactory.Create('customer');
GetCustomers();
function GetCustomers() {
dataService.read().then(function (data) {
vm.customer = data.fields;
}
});
}
return vm;
}
})();
Factory Method
(function () {
'use strict';
angular
.module('app.factory')
.factory('custFactory', custFactory);
custFactory.$inject = ['$q'];
/* #ngInject */
function custFactory($q) {
var _create = function (type) {
var obj = {};
switch (type.toString().toLowerCase()) {
case "customer":
obj = new Customer();
break;
default:
obj = null;
}
return obj;
}
return {
Create: _create
};
}
})();
View Model
function Customer()
{
var dto = this;
dto.Customer = {
"Name" : "",
"Gender" : "", // & so on
}
return dto;
}
If you check my above custCtrl on //line##, I am calling factory method to instantiate customer object as below.
vm.customer= custFactory.Create('customer');
But if I don't create a customer VM & factory and simple assign an empty string as below.
vm.customer= {};
Still its working with no issue.
So my question why should I create a VM & factory?? What is its
benefit??
AngularJS patterns and best practices involve following the MV(whatever) pattern and designing/implementing modular components with high cohesion and low coupling. That way you can easily modify one piece of code without having to make changes in another piece of code.
Typically a factory or service is used as the layer of client-side code that interacts with a RESTful API or some sort of server-side code. Typically that is the only function of the factory. That way you can inject the factory into any controller that needs to use the factory's functions. When you need to modify how you call your API, you only need to make a change in the factory instead of every single controller that would use that function.
Similarly with the view model object you have created, you only need to make changes in one place. Imagine having five different controllers that all need to use a Customer object. Now you decide you want to remove the Name field and replace it with FirstName and LastName. Would you want to go through all your controllers and make that change, or would you want to just change the view model?
To give you the short answer. There's nothing functionally wrong with not creating a view model or a factory/service. From an architectural or design perspective, it makes tons of sense to have a strong separation of concerns in your application. Therefore, it makes sense to use a factory for data access and a view model for your data objects.
Try imagining your application growing to have hundreds of controllers/views and dozens of factories or services. It would be a huge pain to maintain that application if you didn't have these patterns and best practices implemented from the beginning of development.
the controller alias is suitable when you have nested controllers,
while the factory or services are suitable to share data between different controllers.
For example it's useful to have your mehtod CreateCustomer if you use it from different controllers. Since you don't store data in it, it would be better to use a service instead. Anyway it allows you to save code.
EXAMPLE
var app = angular.module("myApp", []);
app.factory("myFactory", function(){
var private = {
users: {}
};
var data = {
getUser: function(id){
return private.users[id];
},
createUser: function(id){
private.users[id] = someData;
return private.currentUser;
}
};
return data;
});
app.controller("myC1", ["myFactory", function(myFactory){
$scope.user = myFactory.createUser(1);
}]);
app.controller("myC2", ["myFactory", function(myFactory){
$scope.user = myFactory.getUser(1);
}]);
As you see the two controllers can access the same data

Angular: circular dependency of specific case

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

Set Angularjs constants at project startup from $http.GET

In my application I have an orchestration service that gets the uris from different services that register with it. service_1 and service_2 could be on different machines but one registered, the uris of their machines will be stored.
In my other application which makes use of that orchestration service, I want to call to the orchestration service to get the uris to use, but then I want to set them as Angular constants, or at least be able to use the uri's values.
So this is the service that's going to be using the 'constant' which is the uri pulled from orchestration service:
(function () {
'use strict';
angular
.module('data.model-view', ['restapi'])
.factory('MVService', MVService);
MVService.$inject = ['$http', '$q', 'exception', 'logger', 'restapi'];
/* #ngInject */
function MVService($http, $q, exception, logger, restapi) {
var HOST = restapi.mvservice.HOST;
var MODULE = restapi.mvservice.MODULE;
...
//below is an example of what will use the above host/module in order to
//get the data needed for this application
function getModels() {
return $http.get(HOST + MODULE + '/model/retrieveAll')
.then(success)
.catch(fail);
function success(response) {
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed for retrieveAll')(e);
}
}
So then this is restapi module where I'd like the constants to be set, so I can have access to them throughout the application, but I need to get them from the orchestration service first.
(function() {
'use strict';
var data = '';
angular
.module('restapi', [])
.factory('restapi', function($http, exception) {
var HOST = %%ORCSERVICE%%;
var MODULE = '/orchestration/service/rest/data';
return $http.get(HOST + MODULE)
.then(success)
.catch(fail);
function success(response) {
//so at this point I can actually access the data I need
//with a console.debug(response.data);
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed to reach orc')(e);
}
}).constant('restapi', constant());
function constant() {
//set constants here based on the data pulled from above
//ideally I want the result of this to be like
//{
// mvservice: {
// 'HOST': 'http://xxxxxxxxxx.amazonaws.com'
// 'MODULE': '/rest/service/whatever'
// },
// ... //other service here
//}
}
})();
Like I say in the comment above, I can actually get the data I need (the uris) from the $http.get immediately above. I'd just like then to be able to get the data set as a constant, or at least in a form that I can access it. Because when MVService spins up, it needs the its own uir from the orchestration service in order to be able to make its rest calls. Sorry might be a little confusing, let me know if there is a need for clarification.
Try bootstrapping app after getting necessary data:
var injector = angular.injector(['ng']),
http = injector.get('$http');
http.get(HOST + MODULE).then(function (result) {
app.value('restapi', result.data);
angular.bootstrap(document, [app.name]);
});
There is multiple way :
If you want to use angular.constant you can do it by getting the url by delaying bootstrap of angular until you get your values. Because you can't set constant once the ng-app has loaded. To do this see karaxuna's answer.
Another way is to perform your angular queries in the constructor of your service. Because the routing and call of controllers won't happen until all promise of the service's instantiation phase will be resolved. So you can store the result of your request as fields of your service and then access it without any racing problems on the controllers/directives/angular.run parts of your application.

AngularJS controllers, design pattern for a DRY code

I have created a full example for the purpose of describing this issue. My actual application is even bigger than the presented demo and there are more services and directives operated by every controller. This leads to even more code repetition. I tried to put some code comments for clarifications,
PLUNKER: http://plnkr.co/edit/781Phn?p=preview
Repetitive part:
routerApp.controller('page1Ctrl', function(pageFactory) {
var vm = this;
// page dependent
vm.name = 'theOne';
vm.service = 'oneService';
vm.seriesLabels = ['One1', 'Two1', 'Three1'];
// these variables are declared in all pages
// directive variables,
vm.date = {
date: new Date(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
})
Basically I moved all the logic I could to factories and directives. But now in every controller that uses certain directive I need, for example, a field that keeps the value that directive is modifying. And it's settings. Later I need similar field to keep the data that comes from dataservice, and the call itself (method) is the same as well.
This leads to a lot of repetition.
Graphically I see the current example to look like this:
While I believe the proper design should look more like this:
I tried to find some solution here, but none seem to be confirmed. What I have found:
AngularJS DRY controller structure, suggesting I pass the $scope or vm and decorate it with extra methods and fields. But many sources say it is dirty solution.
What's the recommended way to extend AngularJS controllers? using angular.extend, but this have problems when using controller as syntax.
And then I have found also the answer (in the link above):
You don't extend controllers. If they perform the same basic functions then those functions need to be moved to a service. That service can be injected into your controllers.
And even when I did there is still a lot of repetition. Or is it the way it just has to be? Like John Papa sais (http://www.johnpapa.net/angular-app-structuring-guidelines/):
Try to stay DRY (Don't Repeat Yourself) or T-DRY
Did you face a similar issue? What are the options?
From a over all design perspective I don't see much of a difference between decorating a controller and extending a controller. In the end these are both a form of mixins and not inheritance. So it really comes down to what you are most comfortable working with. One of the big design decisions comes down to not just how to pass in functionality to just all of the controllers, but how to also pass in functionality to say 2 out of the 3 controllers also.
Factory Decorator
One way to do this, as you mention, is to pass your $scope or vm into a factory, that decorates your controller with extra methods and fields. I don't see this as a dirty solution, but I can understand why some people would want to separate factories from their $scope in order to separate concerns of their code. If you need to add in additional functionality to the 2 out of 3 scenario, you can pass in additional factories. I made a plunker example of this.
dataservice.js
routerApp.factory('pageFactory', function() {
return {
setup: setup
}
function setup(vm, name, service, seriesLabels) {
// page dependent
vm.name = name;
vm.service = service;
vm.seriesLabels = seriesLabels;
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = get(vm.service);
}
//default call
vm.update();
}
});
page1.js
routerApp.controller('page1Ctrl', function(pageFactory) {
var vm = this;
pageFactory.setup(vm, 'theOne', 'oneService', ['One1', 'Two1', 'Three1']);
})
Extending controller
Another solution you mention is extending a controller. This is doable by creating a super controller that you mix in to the controller in use. If you need to add additional functionality to a specific controller, you can just mix in other super controllers with specific functionality. Here is a plunker example.
ParentPage
routerApp.controller('parentPageCtrl', function(vm, pageFactory) {
setup()
function setup() {
// these variables are declared in all pages
// directive variables,
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
}
//default call
vm.update();
}
})
page1.js
routerApp.controller('page1Ctrl', function($controller) {
var vm = this;
// page dependent
vm.name = 'theOne';
vm.service = 'oneService';
vm.seriesLabels = ['One1', 'Two1', 'Three1'];
angular.extend(this, $controller('parentPageCtrl', {vm: vm}));
})
Nested States UI-Router
Since you are using ui-router, you can also achieve similar results by nesting states. One caveat to this is that the $scope is not passed from parent to child controller. So instead you have to add the duplicate code in the $rootScope. I use this when there are functions I want to pass through out the whole program, such as a function to test if we are on a mobile phone, that is not dependent on any controllers. Here is a plunker example.
You can reduce a lot of your boilerplate by using a directive. I've created a simple one to replace all of your controllers. You just pass in the page-specific data through properties, and they will get bound to your scope.
routerApp.directive('pageDir', function() {
return {
restrict: 'E',
scope: {},
controller: function(pageFactory) {
vm = this;
vm.date = {
date: moment().startOf('month').valueOf(),
dateOptions: {
formatYear: 'yy',
startingDay: 1
},
format: 'dd-MMMM-yyyy',
opened: false
};
vm.open = function($event) {
vm.date.opened = true;
};
// dataservice
vm.data = []; // the structure can be different but still similar enough
vm.update = function() {
vm.data = pageFactory.get(vm.service);
};
vm.update();
},
controllerAs: 'vm',
bindToController: {
name: '#',
service: '#',
seriesLabels: '='
},
templateUrl: 'page.html',
replace: true
}
});
As you can see it's not much different than your controllers. The difference is that to use them, you'll use the directive in your route's template property to initialize it. Like so:
.state('state1', {
url: '/state1',
template: '<page-dir ' +
'name="theOne" ' +
'service="oneService" ' +
'series-labels="[\'One1\', \'Two1\', \'Three1\']"' +
'></page-dir>'
})
And that's pretty much it. I forked your Plunk to demonstrate.
http://plnkr.co/edit/NEqXeD?p=preview
EDIT: Forgot to add that you can also style the directive as you wish. Forgot to add that to the Plunk when I was removing redundant code.
I can't respond in comment but here what i will do :
I will have A ConfigFactory holding a map of page dependent variables :
{
theOne:{
name: 'theOne',
service: 'oneService',
seriesLabels: ['One1', 'Two1', 'Three1']
},
...
}
Then i will have a LogicFactory with a newInstance() method to get a proper object each time i need it.
The logicFactory will get all the data / method shared betwwen controllers.
To this LogicFactory, i will give the view-specific data. and the view will have to bind to this Factory.
And to retrieve the view-specific data i will pass the key of my configuration map in the router.
so let say the router give you #current=theOne, i will do in the controller :
var specificData = ServiceConfig.get($location.search().current);
this.logic = LogicFactory.newInstance(specificData);
Hope it help
I retouch your example, here is the result : http://plnkr.co/edit/ORzbSka8YXZUV6JNtexk?p=preview
Edit: Just to say this way, you can load the specific configuration from a remote server serving you the specific-view data
I faced completely the same issues as you described. I'm a very big supporter of keeping things DRY. When I started using Angular there was no prescribed or recommended way to do this, so I just refactored my code as I went along. As with many things I dont think their is a right or wrong way to do these things, so use whichever method you feel comfortable with. So below is what I ended up using and it has served me well.
In my applications I generally have three types of pages:
List Page - Table list of specific resource. You can
search/filter/sort your data.
Form Page - Create or Edit resource.
Display Page - Detailed view-only display page of resource/data.
I've found there are typically a lot of repetitive code in (1) and (2), and I'm not referring to features that should be extracted to a service. So to address that I'm using the following inheritance hierarchy:
List Pages
BaseListController
loadNotification()
search()
advancedSearch()
etc....
ResourceListController
any resource specific stuff
Form Pages
BaseFormController
setServerErrors()
clearServerErrors()
stuff like warn user is navigating away from this page before saving the form, and any other general features.
AbstractFormController
save()
processUpdateSuccess()
processCreateSuccess()
processServerErrors()
set any other shared options
ResourceFormController
any resource specific stuff
To enable this you need some conventions in place. I typically only have a single view template per resource for Form Pages. Using the router resolve functionality I pass in a variable to indicate if the form is being used for either Create or Edit purposes, and I publish this onto my vm. This can then be used inside your AbstractFormController to either call save or update on your data service.
To implement the controller inheritance I use Angulars $injector.invoke function passing in this as the instance. Since $injector.invoke is part of Angulars DI infrastructure, it works great as it will handle any dependencies that the base controller classes need, and I can supply any specific instance variables as I like.
Here is a small snippet of how it all is implemented:
Common.BaseFormController = function (dependencies....) {
var self = this;
this.setServerErrors = function () {
};
/* .... */
};
Common.BaseFormController['$inject'] = [dependencies....];
Common.AbstractFormController = function ($injector, other dependencies....) {
$scope.vm = {};
var vm = $scope.vm;
$injector.invoke(Common.BaseFormController, this, { $scope: $scope, $log: $log, $window: $window, alertService: alertService, any other variables.... });
/* ...... */
}
Common.AbstractFormController['$inject'] = ['$injector', other dependencies....];
CustomerFormController = function ($injector, other dependencies....) {
$injector.invoke(Common.AbstractFormController, this, {
$scope: $scope,
$log: $log,
$window: $window,
/* other services and local variable to be injected .... */
});
var vm = $scope.vm;
/* resource specific controller stuff */
}
CustomerFormController['$inject'] = ['$injector', other dependencies....];
To take things a step further, I found massive reductions in repetitive code through my data access service implementation. For the data layer convention is king. I've found that if you keep a common convention on your server API you can go a very long way with a base factory/repository/class or whatever you want to call it. The way I achieve this in AngularJs is to use a AngularJs factory that returns a base repository class, i.e. the factory returns a javascript class function with prototype definitions and not an object instance, I call it abstractRepository. Then for each resource I create a concrete repository for that specific resource that prototypically inherits from abstractRepository, so I inherit all the shared/base features from abstractRepository and define any resource specific features to the concrete repository.
I think an example will be clearer. Lets assume your server API uses the following URL convention (I'm not a REST purest, so we'll leave the convention up to whatever you want to implement):
GET -> /{resource}?listQueryString // Return resource list
GET -> /{resource}/{id} // Return single resource
GET -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT -> /{resource}/{id} // Update existing resource
POST -> /{resource}/ // Create new resource
etc.
I personally use Restangular so the following example is based on it, but you should be able to easily adapt this to $http or $resource or whatever library you are using.
AbstractRepository
app.factory('abstractRepository', [function () {
function abstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
abstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + 'view').get();
},
update: function (updatedResource) {
return updatedResource.put();
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
}
// etc.
};
abstractRepository.extend = function (repository) {
repository.prototype = Object.create(abstractRepository.prototype);
repository.prototype.constructor = repository;
};
return abstractRepository;
}]);
Concrete repository, let's use customer as an example:
app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {
function customerRepository() {
abstractRepository.call(this, restangular, 'customers');
}
abstractRepository.extend(customerRepository);
return new customerRepository();
}]);
So now we have common methods for data services, which can easily be consumed in the Form and List controller base classes.
To summarize the previous answers:
Decorating controllers: as you said, this is a dirty solution; Imagine having different factories decorating the same controller, it will be very difficult (especially for other developers) to prevent collision of properties, and equally difficult to trace which factory added which properties. It's actually like having multiple inheritance in OOP, something that most modern languages prevent by design for the same reasons.
Using a directive: this can be a great solution if all your controllers are going to have the same html views, but other than that you will have to include fairly complex logic in your views which can be difficult to debug.
The approach I propose is using composition (instead of inheritance with decorators). Separate all the repetitive logic in factories, and leave only the creation of the factories in the controller.
routerApp.controller('page1Ctrl', function (Page, DateConfig, DataService) {
var vm = this;
// page dependent
vm.page = new Page('theOne', 'oneService', ['One1', 'Two1', 'Three1']);
// these variables are declared in all pages
// directive variables,
vm.date = new DateConfig()
// dataservice
vm.dataService = new DataService(vm.page.service);
//default call
vm.dataService.update();
})
.factory('Page', function () {
//constructor function
var Page = function (name, service, seriesLabels) {
this.name = name;
this.service = service;
this.seriesLabels = seriesLabels;
};
return Page;
})
.factory('DateConfig', function () {
//constructor function
var DateConfig = function () {
this.date = new Date();
this.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
this.format = 'dd-MMMM-yyyy';
this.opened = false;
this.open = function ($event) {
this.opened = true;
};
};
return DateConfig;
})
This code is not tested, but I just want to give an idea. The key here is to separate the code in the factories, and add them as properties in the controller. This way the implementation is not repeated (DRY), and everything is obvious in the controller code.
You can make your controller even smaller by wrapping all the factories in a larger factory (facade), but this may make them more tightly coupled.

AngularJS trigger and watch object value change in service from controller from another module (Extended)

Referring to this question
AngularJS trigger and watch object value change in service from controller
It is trying to watch for changes in a service from a controller.
Im trying to extend it to handle multiple module (concurrent app in same page's different div's) communication.
Problem:
I want to achieve the similar feat, but with slight different scenario. I have two modules myApp and yourApp for example. myApp has a service. I want to watch changes in myApp's service within yourApp module's controller. Can it be done? or is there different method to reflect and detect data changes of one module inside another module.
Consider the example code below:
HTML
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-click="setFTag()">Click Me</div>
</div>
</div>
<div ng-app="yourApp">
<div ng-controller="yourCtrl">
</div>
</div>
Javascript
// myApp module
var myApp = angular.module('myApp',[]);
// myApp's service
myApp.service('myService', function() {
this.tags = {
a: true,
b: true
};
this.setFalseTag = function() {
alert("Within myService->setFalseTag");
this.tags.a = false;
this.tags.b = false;
//how do I get the watch in YourCtrl of yourApp module to be triggered?
};
});
// myApp's controller
myApp.controller('MyCtrl', function($scope, myService) {
//$scope.myService = myService;
$scope.setFTag = function() {
alert("Within MyCtrl->setFTag");
myService.setFalseTag();
};
/*
$scope.$watch(function () {
return myService.tags;
}, function(newVal, oldVal) {
alert("Inside watch");
console.log(newVal);
console.log(oldVal);
}, true);
*/
});
// yourApp module (Injecting myApp module into yourApp module)
var yourApp = angular.module('yourApp',['myApp']);
// yourApp's controller
yourApp.controller('YourCtrl', function($scope, myService) {
$scope.$watch(function () {
return myService.tags;
}, function(newVal, oldVal) {
alert("Inside watch of yourcontroller");
console.log(newVal);
console.log(oldVal);
}, true);
});
Note: I am not sure if this is the right way to communicate between modules. Any suggestion or the solution will be highly appreciated.
P.S. Ive bootstrapped two modules to fit into same page.
JSFiddle
Communicating between modules is a whole different story from communicating between apps.
In the case of communicating between modules, it's only a matter of injecting module A into module B.
In which case, module B has complete access to inject/communicate with anything exposed in module A.
Communicating between apps however, is somewhat more complicated. You would need to setup something 'outside' the angular world, accepting properties from both applications.
I've put together a jsBin, showcasing how you can do it.
With that said, I'm not sure I would recommend it - but then again best practices on the subject do not exist afaik.
The gist of it is;
Setup a new instance of a shared service that lives outside the Angular world.
Attach the instance of said service to window.
Access the above instance in your app specific services/controllers through $window.
Register each $scope that needs to access the data stored in the shared service.
Upon updating data in the shared service, loop through the registered $scopes and trigger an $evalAsync(so as to not end up with $digest already in progress).
Watch as your data is synced across applications.
This is just a PoC on how to do it, I would not recommend it as it sort of blows when it comes to unit testing. And also because we are exposing properties on window. yuck.
Sure, you could disregard from exposing on window - but then your code would have to live in the same file (afaic). Yet another, yuck.
To build upon this, if you were to decide (or, be able to) only use a single app (multiple modules is just fine) and want to communicate/sync a service with multiple components I reckon this would be the best way to do so:
app.service('shared', function () {
var data = {
a: true,
b: true
};
this.toggle = function() {
data.a = !data.a;
data.b = !data.b;
};
Object.defineProperty(this, 'data', {
get: function () {
return data;
}
});
});
By using an object getter, you won't need to setup a $watch to sync data across your components within the same module. Nor trigger manual $digest's.
another jsbin - showcasing the above.
The difference between two modules and two apps as I see it:
2 modules
Two separate modules that gets 'bundled' together into a single application (either by a third module, or injecting module A into B).
var app1 = angular.module('app1', ['app2', 'app3']);
var app2 = angular.module('app2', []);
var app3 = angular.module('app3', []);
angular.bootstrap(/*domElement*/, app1);
2 Apps
var app1 = angular.module('app1', []);
var app2 = angular.module('app2', []);
angular.bootstrap(/*domElement1*/, app1);
angular.bootstrap(/*domElement2*/, app2);
I don't think there is any point in having two applications and share state between the two. I think the whole point with running two applications is to separate the two. Otherwise it's just over engineering imho.
Some thoughts:
You could add a common dependency to both applications. It probably wont be a shared state, but you would have access to the same implementation in both apps.
You could possibly utilise sessionStorage as a medium of transportation for your data between the two applications. Just make sure to cleanup afterwards :)

Categories

Resources