Where to put model data and behaviour? [tl; dr; Use Services]
Asked Answered
J

7

342

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?

Joo answered 20/6, 2012 at 3:56 Comment(2)
Resource objects are basically the models in Angular.js.. am extending them.Magnetoelectricity
"Your model lives on the server" ---> from this interesting read: wekeroad.com/2013/04/25/models-and-services-in-angularFe
A
155

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
}
Alesandrini answered 20/6, 2012 at 13:31 Comment(9)
What would be the benefit of using a service over just creating a plain Javascript object as a model and assigning this to the controller scope?Joo
Incase you need the same logic shared between multiple controllers. Also, this way it's easier to test things independently.Alesandrini
Thanks again Andy. Sharing logic between multiple controllers would be the same if I create s singleton without using angularjs's services. But testing is probably a good reasons to do it that way - in case the model/service as it's own dependecies.Joo
Yeah, with a plain old Javascript object you wouldn't be able to inject anything Angular into your ListService. Like in this example, if you needed to $http.get to retrieve the List data at the start, or if you needed to inject $rootScope so you could $broadcast events.Alesandrini
To make this example more DCI like shouldn't the data be outside of ListService?Palmira
What would be the benefit of using such list in a data-binding framework like angular where I may want to ng-repeat that list?Fasces
@AndyJoslin you have declared 'list' variable inside services.so your service now has state.Is it best practice to have state to service? as per my knowledge service should be stateless..Plz correct me .Torn
@AndyJoslin Question: If my controller and Model is separate, should i be getting the data first in the model from a service and then pass it to the controller or directly from the controller ? In the 1st case all the data used by the controller would be present in the model.Aney
Services should be a THIN layer exposing your interface. You should stive to have as little logic in the actual service layer as possible. It should only be calling other layers to do validation, getting/updating data through a gateway pattern or something but do not put a ton of business logic or other logic in your services layer.Aurelia
S
81

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;
});
Sexism answered 13/2, 2013 at 11:22 Comment(3)
Looks like a good solution to me. Would use it except I would use ElementFactoryService and call a create/build method to validate raw data and get the actual new Element instance.Antonio
@BenG - Why are using extend? Why not just: this.id = null; collection1 = []?Gaily
Wouldn't having an appropriately named ElementService for each collection result in a bunch of nearly-identical files?Dishrag
A
29

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.

Artefact answered 30/5, 2013 at 10:30 Comment(2)
You should provide a link to where this is stated in the documentation. I did a Google search for "Angular makes no restrictions or requirements on the model", and it doesn't turn up anywhere in the official docs, as far as I can tell.Leach
it was in the old angularjs docs (the one alive while answering): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…Artefact
M
8

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);
   };
}
Mediocrity answered 20/6, 2012 at 5:56 Comment(1)
Thanks for elaborating the DCI stuff. It is a great read. But my questions really aims at "where to put the model objects in angularjs". DCI is just in there for reference, that I might not only have a model, but split it in the DCI way. Will edit the question to make it more clear.Joo
R
5

As stated by other posters, Angular provides no out-of-the-box base class for modeling, but one can usefully provide several functions:

  1. Methods for interacting with a RESTful API and creating new objects
  2. Establishing relationships between models
  3. Validating data before persisting to the backend; also useful for displaying real-time errors
  4. Caching and lazy-loading to keep from making wasteful HTTP requests
  5. State machine hooks (before/after save, update, create, new, etc)

One library that does all of these things well is ngActiveResource (https://github.com/FacultyCreative/ngActiveResource). Full disclosure--I wrote this library--and I have used it successfully in building several enterprise-scale applications. It's well tested, and provides an API that should be familiar to Rails developers.

My team and I continue to actively develop this library, and I'd love to see more Angular developers contribute to it and battle test it.

Ranunculaceous answered 11/1, 2014 at 7:44 Comment(1)
I as just looking at your post and was wondering what the differences between your ngActiveResource and Angular's $resource service. I'm a little new to Angular, and quickly browsed both sets of docs, but they seem to offer a lot of overlap. Was ngActiveResource developed prior to the $resource service being available?Janes
G
5

An older question, but I think the topic is more relevant than ever given the new direction of Angular 2.0. I would say a best practice is to write code with as few dependencies on a particular framework as possible. Only use the framework specific parts where it adds direct value.

Currently it seems like the Angular service is one of the few concepts that will make it to the next generation of Angular, so it's probably smart to follow the general guideline of moving all logic to services. However, I would argue that you can make decoupled models even without a direct dependency on Angular services. Creating self contained objects with only necessary dependencies and responsibilities is probably the way to go. It also makes life a lot easier when doing automated testing. Single responsibility is a buzz work these days, but it does make a lot of sense!

Here is an example of a patter I consider good for decoupling the object model from the dom.

http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

A key goal is to structure your code in a way that makes it just as easy to use from a unit tests as from a view. If you achieve that you are well positioned to write realistic and useful tests.

Glabrous answered 19/12, 2014 at 16:54 Comment(0)
F
4

I've tried to tackle that exact issue in this blog post.

Basically, the best home for data modeling is in services and factories. However, depending on how you retrieve your data and the complexity of the behaviors you need, there are lots of different ways to go about the implementation. Angular currently has no standard way or best practice.

The post covers three approaches, using $http, $resource, and Restangular.

Here's some example code for each, with a custom getResult() method on the Job model:

Restangular (easy peasy):

angular.module('job.models', [])
  .service('Job', ['Restangular', function(Restangular) {
    var Job = Restangular.service('jobs');

    Restangular.extendModel('jobs', function(model) {
      model.getResult = function() {
        if (this.status == 'complete') {
          if (this.passed === null) return "Finished";
          else if (this.passed === true) return "Pass";
          else if (this.passed === false) return "Fail";
        }
        else return "Running";
      };

      return model;
    });

    return Job;
  }]);

$resource (slightly more convoluted):

angular.module('job.models', [])
    .factory('Job', ['$resource', function($resource) {
        var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: function(data, header) {
                    var wrapped = angular.fromJson(data);
                    angular.forEach(wrapped.items, function(item, idx) {
                        wrapped.items[idx] = new Job(item);
                    });
                    return wrapped;
                }
            }
        });

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    }]);

$http (hardcore):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .factory('Job', function() {
        function Job(data) {
            for (attr in data) {
                if (data.hasOwnProperty(attr))
                    this[attr] = data[attr];
            }
        }

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    });

The blog post itself goes into more detail on the reasoning behind why you might use each approach, as well as code examples of how to use the models in your controllers:

AngularJS Data Models: $http VS $resource VS Restangular

There's a possibility Angular 2.0 will offer a more robust solution to data modeling that gets everyone on the same page.

Fairfield answered 17/7, 2014 at 19:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.