Where to put model data and behaviour? [tl; dr; Use Services]
I am working with AngularJS for my latest project. In the documentation and tutorials all model data is put into the controller scope. I understand that is has to be there to be available for the controller and thus within the corresponding views.
However I dont think the model should actually be implemented there. It might be complex and have private attributes for example. Furthermore one might want to reuse it in another context/app. Putting everything into the controller totally breaks MVC pattern.
The same holds true for the behaviour of any model. If I would use DCI architecture and separate behaviour from the data model, I would have to introduce additional objects to hold the behaviour. This would be done by introducing roles and contexts.
DCI == Data Collaboration Interaction
Of course model data and behaviour could be implemented with plain javascript objects or any "class" pattern. But what would be the AngularJS way to do it? Using services?
So it comes down to this question:
How do you implement models decoupled from the controller, following AngularJS best practices?
Solution 1:
You should use services if you want something usable by multiple controllers. Here's a simple contrived example:
myApp.factory('ListService', function() {
var ListService = {};
var list = [];
ListService.getItem = function(index) { return list[index]; }
ListService.addItem = function(item) { list.push(item); }
ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
ListService.size = function() { return list.length; }
return ListService;
});
function Ctrl1($scope, ListService) {
//Can add/remove/get items from shared list
}
function Ctrl2($scope, ListService) {
//Can add/remove/get items from shared list
}
Solution 2:
I'm currently trying this pattern, which, although not DCI, provides a classical service / model decoupling (with services for talking to web services (aka model CRUD), and model defining the object properties and methods).
Note that i only use this pattern whenever the model object needs methods working on its own properties, that i'll probably use everywhere (such as improved getter/setters). I'm not advocating doing this for every service systematically.
EDIT: I used to think this pattern would go against the "Angular model is plain old javascript object" mantra, but it seems to me now that this pattern is perfectly fine.
EDIT (2): To be even clearer, I use a Model class only to factor simple getters / setters (e.g. : to be used in view templates). For big business logic, i recommend using separate service(s) that "know" about the model, but are kept separated from them, and only include business logic. Call it a "business expert" service layer if you want
service/ElementServices.js (notice how Element is injected in the declaration)
MyApp.service('ElementServices', function($http, $q, Element)
{
this.getById = function(id)
{
return $http.get('/element/' + id).then(
function(response)
{
//this is where the Element model is used
return new Element(response.data);
},
function(response)
{
return $q.reject(response.data.error);
}
);
};
... other CRUD methods
}
model/Element.js (using angularjs Factory, made for object creation)
MyApp.factory('Element', function()
{
var Element = function(data) {
//set defaults properties and functions
angular.extend(this, {
id:null,
collection1:[],
collection2:[],
status:'NEW',
//... other properties
//dummy isNew function that would work on two properties to harden code
isNew:function(){
return (this.status=='NEW' || this.id == null);
}
});
angular.extend(this, data);
};
return Element;
});
Solution 3:
The Angularjs documentation clearly states:
Unlike many other frameworks Angular makes no restrictions or requirements on the model. There are no classes to inherit from or special accessor methods for accessing or changing the model. The model can be primitive, object hash, or a full object Type. In short the model is a plain JavaScript object.
— AngularJS Developer Guide - V1.5 Concepts - Model
So it means that's up to you how to declare a model. It's a simple Javascript object.
I personally won't use Angular Services as they were meant to behave like singleton objects you can use, for example, to keep global states across your application.
Solution 4:
DCI is a paradigm and as such there's no angularJS way of doing it, either the language support DCI or it doesn't. JS support DCI rather well if you are willing to use source transformation and with some drawbacks if you are not. Again DCI has no more to do with dependency injection than say a C# class has and is definitely not a service either. So the best way to do DCI with angulusJS is to do DCI the JS way, which is pretty close to how DCI is formulated in the first place. Unless you do source transformation, you will not be able to do it fully since the role methods will be part of the object even outside the context but that's generally the problem with method injection based DCI. If you look at fullOO.info the authoritative site for DCI you could have a look at the ruby implementations they also use method injection or you could have a look at here for more information on DCI. It's mostly with RUby examples but the DCI stuff is agnostic to that. One of the keys to DCI is that what the system does is separated from what the system is. So the data object are pretty dumb but once bound to a role in a context role methods make certain behaviour available. A role is simply an identifier, nothing more, an when accessing an object through that identifier then role methods are available. There's no role object/class. With method injection the scoping of role methods is not exactly as described but close. An example of a context in JS could be
function transfer(source,destination){
source.transfer = function(amount){
source.withdraw(amount);
source.log("withdrew " + amount);
destination.receive(amount);
};
destination.receive = function(amount){
destination.deposit(amount);
destination.log("deposited " + amount);
};
this.transfer = function(amount){
source.transfer(amount);
};
}
Solution 5:
This article about models in AngularJS might help:
http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/