AngularJS controllers, design pattern for a DRY code - javascript

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.

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

Save form data in view

how can i save data from 1 view to another in angularjs?
i did $rootScope
From what I see, you use 2 different controllers for each view (or one for the view and none for the root view).
The problem is that Angular can't share data between controllers like that.
You either have to use a service/factory, or use the rootscope, but not as you did, rather with broadcast and emit
If I were you I would use a service.
EDIT Here you go, a service for you :
(function() {
'use strict';
angular
.module('YourModuleName')
.factory('CountriesService', CountriesService);
CountriesService.$inject = ['Your', 'dependencies', 'here', 'in', 'string'];
/* #ngInject */
function CountriesService(your, dependencies, here, not, in, string) {
var service = {
setCountries: setCountries,
getCountries: getCountries
};
var vm = this;
vm.countries = []; // Or maybe an object ?
// ... List of other variables you need to store.
return service;
////////////////
function setCountries(listOfCountries) {
vm.countries = listOfCountries;
}
function getCountries() {
return vm.countries;
}
}
})();
This will store your variables. In your controller you add CountriesService as a dependency, to save you use CountriesService.setCountries and to load you use CountriesService.getCountries. Be aware that refreshing the page will delete all the data !
EDIT NUMBER 2
If you're scared of John papa guidelines, here is a simple service you can use in the same file you put your controller :
app.factory('CountryControl', function(your, dependencies) {
var service = {
setCountries: setCountries,
getCountries: getCountries
};
this.countries = []; // Or maybe an object ?
// ... List of other variables you need to store.
return service;
////////////////
function setCountries(listOfCountries) {
this.countries = listOfCountries;
}
function getCountries() {
return this.countries;
}
});
I have an app that does this more or less. A service fixes this nicely AND creates a mechanism such that you can do this anywhere in your app.
First, I would recommend not trying to manage this with scope. Just put an object on your controller (myFormObj), and add the properties you want to it (name, rank, serialnumber, etc).
Then bind the input fields of the form, to the properties in that object (as opposed to scope vars). So your ng-model things would look like myCtl.formObj.name, and so on.
When the user triggers the event that changes the view, save a COPY (angular.copy) of that formObj off to the side, usually in a Service (think FormStateService or something). FormStateService could do nothing more than hold a simple array.
this.forms = { 'TheNameOfYourForm' : theFormObjToSave };
So, when the user triggers that event that leaves the form, you just do this:
formStateSvc.forms [ 'NameOfMyForm' ] = angular.copy ( theFormObj );
When the user comes back to the original view and the controller initializes, you just ask the formStateSvc:
if ( 'NameOfMyForm' in formStateSvc.forms ) {
this.formObj = formStateSvc.forms [ 'NameOfMyForm' ];
}
Voila, your old form state is restored.
More robustly, you could create "addForm, removeForm" methods etc, you could ensure against things like undefined, and you could make the rebind to the former state implicit (when your form's controller inits, just ask it to restore the state if there's any to restore). So your controller would just have:
this.formObj = formStateSvc.rebindOldDataIfItExists ( 'MyFormName' );
You get the idea.
A simple approach is to create a value provider object and publish it on scope:
//Create value provider object
app.value("FormObj", {});
app.controller("myController", function($scope, FormObj) {
//Publish on scope
$scope.FormObj = FormObj;
});
Then have the ng-model directives use that object:
Name <input ng-model="FormObj.name"><br>
Rank <input ng-model="FormObj.rank"><br>
SerialNum <input ng-model="FormObj.ssnum"><br>
The value object is a singleton which persists for the life of the application. Changes to the contents of the object will be retained and available to other controllers and will survive changes to the view.
The DEMO on PLNKR

Shared variables and multiple controllers AngularJS

I have multiple controllers on a small app I'm writing, and I have successfully shared a 'selected' variable between the controllers like so.
app.service('selectedEmployee', function () {
var selected = null;
return
{
getSelected: function() {
return selected;
},
postSelected: function(employee) {
selected = employee;
}
};
});
I have a side nav bar with a list of employees. When I click on an employee I call the postSelected function then the getSelected to set $scope.selected.
$scope.selectEmployee = function(employee) {
//Calling Service function postSelected
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
I have a third controller for my main content area, and this is where I don't understand what to do. I want information from the selected employee to be displayed, but angular is compiling the whole page before the first employee has a chance to get set as selected, and subsequent selections of an employee aren't reloading the main content page (because I haven't told them to I think). Here's my main content controller:
app.controller('mainContentController', ['$scope','selectedEmployee',
function ($scope, selectedEmployee) {
$scope.selected = selectedEmployee.getSelected();
console.log($scope.selected);
}
]);
My main content view is very simple right now
<h2>{{selected.firstName}}{{selected.lastName}}</h2>
My question is how I can tell one controller to effectively update its partial view so that when I select an employee it displays information.
GitLab repo
Don't rely on messy broadcasts if your goal is simply to display & modify the data in the controller's template.
Your controllers do NOT need to "know" when the Service or Factory has updated in order to use it in the template as Angular will handle this for you, as you access the data via dot notation. This is the important concept which you should read more about.
This Fiddle shows both ways of accessing the data, and how using the container object in the template causes Angular to re-check the same actual object on changes - instead of the primitive string value stored in the controller:
http://jsfiddle.net/a01f39Lw/2/
Template:
<div ng-controller="Ctrl1 as c1">
<input ng-model="c1.Bands.favorite" placeholder="Favorite band?">
</div>
<div ng-controller="Ctrl2 as c2">
<input ng-model="c2.Bands.favorite" placeholder="Favorite band?">
</div>
JS:
var app = angular.module("app", []);
app.factory('Bands', function($http) {
return {
favorite: ''
};
});
app.controller('Ctrl1', function Ctrl1(Bands){
this.Bands = Bands;
});
app.controller('Ctrl2', function Ctrl2(Bands){
this.Bands = Bands;
});
First of all lets start by good practices, then solve your problem here...
Good Practices
At least by my knowledge, i dont intend to use services the way you do... you see, services are more like objects. so if i were to convert your service to the way i normally use it would produce the following:
app.service('selectedEmployee', [selectedEmployeeService])
function selectedEmployeeService(){
this.selected = null;
this.getSelected = function(){
return this.selected;
}
this.postSelected = function(emp){
this.selected = emp;
}
}
You see there i put the function seperately, and also made the service an actual object.. i would reccomend you format your controller function argument like this... If you want to disuss/see good practices go here. Anways enough about the good practices now to the real problem.
Solving the problem
Ok The Andrew actually figured this out!! The problem was:that he need to broadcast his message using $rootScope:
$rootScope.$broadcast('selected:updated', $scope.selected);
And then you have to check when $scope.selected is updated.. kinda like $scope.$watch...
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
After that it autmoatically updates and works! Hope this helped!
PS: Did not know he anwsered already...
So after much research and a lot of really great help from Dsafds, I was able to use $rootScope.$broadcast to notify my partial view of a change to a variable.
If you broadcast from the rootScope it will reach every child controller and you don't have to set a $watch on the service variable.
$scope.selectEmployee = function(employee) {
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
$rootScope.$broadcast('selected:updated', $scope.selected);
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
And in the controller of the main content area
function ($scope) {
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
}
I don't think you have to pass the data directly, you could also just as easily call selectedEmployee.getSelected()
$rootScope also has to be included in the Parent controller and the broadcasting controller.

Using $http outside of controllers?

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!

Tell AngularJs to return a new instance via Dependency Injection

The use case is simple: I have two controllers sharing the same dependency MyService. This service is holding some state, lets sat myVariable. If I set it from ControllerOne, then it will be also spotted by ControllerTwo.
What I want is for each controller to have it's own instance of MyService, so that myVariable can be changed by each Controller without affecting the other.
To put it in another words - I want new instance to be returned by Dependency Injection, rather than singleton.
Not as directly has you might hope. Service instances are created the first time they're retrieved by the injector and maintained by the injector... in other words, they're always singletons. The magic happens in here, particularly look at the provider function, which puts the provider instance in the providerCache object.
But don't lose hope, you could just as easily add constructors for whatever it is you want to share in a Service, if you so chose:
app.factory('myService', [function() {
var i = 1;
function Foo() {
this.bar = "I'm Foo " + i++;
};
return {
Foo: Foo
};
}]);
app.controller('Ctrl1', function($scope, myService) {
$scope.foo = new myService.Foo();
console.log($scope.foo.bar) //I'm Foo 1
});
app.controller('Ctrl2', function($scope, myService) {
$scope.foo = new myService.Foo();
console.log($scope.foo.bar) //I'm Foo 2
});
EDIT: as the OP pointed out, there is also the $injector.instantiate, which you can use to call JavaScript constructors outside of your controller. I'm not sure what the implications are of the testability here, but it does give you another way to inject code that will construct a new object for you.
There are plenty of general options for modularizing JavaScript code, but if you want to do something that relies solely on AngularJS, this pattern works:
1) First define the class that you want to inject multiple times:
function MyClass() {
// ...
}
MyClass.prototype.memberMethod = function() {
// ...
}
2) Next, make the constructor available as a constant:
angular.module('myModule').constant('MyClass', MyClass);
3) Finally, use the factory pattern to create named, injectable instances of the class:
angular.module('myOtherModule', ['myModule']).factory('instanceOfMyClass', [
'MyClass',
function(MyClass) { return new MyClass(); }
]);

Categories

Resources