Article Image
read

It can be confusing when trying to structure a client-side application, especially when it comes to separating models from controllers and services. Doing this in Angular means diving into some details.

Image from http://union-bulletin.com/news/2012/aug/03/one-part-science-one-part-excitement-equal-success/

What's a Model? What's a Service?

I read over a post by Joel Hooks today called Modeling Data and State in Your AngularJS Application and the idea was put forward that you can clean up your Controllers by (basically) using Angular's service as a model.

The idea is a good one: Clean Controllers are Godly Controllers but there's some confusion in the post that I think will end up spreading.

Straight up-front: All services (service(), factory(), provider()) in Angular are SINGLETONS. It's difficult under any circumstances to consider a Singleton as a model.

There are so many jargon-filled terms disseminated throughout different approaches to software design... but I think we can agree that a model is representative of data (as opposed to a Domain Model which is all of your stuff under a big umbrella).

In the post above, Joel has a list of authors and a quote from each one - a simple data structure:

var fowler = {  
    name: "Fowler",
    quote: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
},
twain = {  
    name: "Twain",
    quote: "Why, I have known clergymen, good men, kind-hearted, liberal, sincere, and all that, who did not know the meaning of a 'flush.' It is enough to make one ashamed of one's species."
};

One might consider this list to be two instances of a single Model, Author:

var Author = function(atts){  
  var self = this;
  var initialSettings = atts || {};
  //initial settings if passed in
  for(var setting in initialSettings){
    if(initialSettings.hasOwnProperty(setting))
      self[setting] = initialSettings[setting];
  };

  //with some logic...
  self.fullName = function(){
    return self.first + " " + self.last;
  }

  //return the scope-safe instance
  return self;
};

Translating this to Joel's code:

var fowler = Author({  
    name: "Fowler",
    quote: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
}),
twain = Author({  
    name: "Twain",
    quote: "Why, I have known clergymen, good men, kind-hearted, liberal, sincere, and all that, who did not know the meaning of a 'flush.' It is enough to make one ashamed of one's species."
});

So far, so good. Unfortunately I think some confusion crept in - here is the full example of Joel's code for his authorListModel:

  angular.module('modelDemo').service("authorListModel", ['$rootScope', function($rootScope) {
      var fowler = {
              name: "Fowler",
              quote: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
          },
          twain = {
              name: "Twain",
              quote: "Why, I have known clergymen, good men, kind-hearted, liberal, sincere, and all that, who did not know the meaning of a 'flush.' It is enough to make one ashamed of one's species."
          },
          //...

      this.list = [fowler, twain, poe, plato];

      this.selectedAuthor = null;
      this.setSelectedAuthor = function(author) {
          if(this.list.indexOf(author) > -1) {
              this.selectedAuthor = author;
              $rootScope.$broadcast('authorModel::selectedAuthorUpdated', author);
          }
      };
  }]);

In the first line we've told Angular that this function is to be used a Service. Services in Angular are Singletons - which can cause all kinds of interesting headaches if you treat that service as a Model.

In Joel's case there's nothing wrong with the code here - except that it's not a Model - it's a Service and before you think I'm nitpicking - Service, in Angular, has a very particular reason for existence and it's not this:

... the “M” in MVC, model classes encapsulate your application’s data and provide an API to access and manipulate that data. The other classes in your application will make requests of models via this API. When data on the model is updated, the model dispatches events that the other classes within your application can react to.

The definition is a good one - unfortunately the code that is being offered to support the definition is actually a Service, not a Model.

So What About a Model?

I get asked this a fair amount and my answer is typically:

Your model lives on the server

hat-tip to Steve Sanderson for this

This works in many ways: it frees you from duplicating code on the client and it keeps secret things secret (such as applying discounts to a shopping cart, etc).

But what about behavior on individual bits of data? Here's where things get interesting with Angular.

Let's say you use Angular's $resource to hook up to an Author resource in the sky somewhere:

var app = angular.module("authorApp", []);  
var api = function($resource){  
  this.authors = $resource("/authors");
}
app.service("Api", api);  

You can use this in your controller like so:

app.controller("AuthorCtrl", function($scope, Api){

  //returns [{first : 'Mark', last : 'Twain', quote : '...'}]
  $scope.authors = Api.authors.query();
});

That will send a GET request off to "/authors" and return a promise that will provide some data on success. Angular works really well with promises so there's no more code to be written here - when the data shows up, it hits the $scope which is "bound" to the UI through ng-repeat and off we go.

But how can we use our Author prototype above? A number of ways - let's go through this...

The first way is to get away from using "service" and have a bit more structure with a factory:

var app = angular.module("authorApp", []);  
var api = function($resource){

  var authorResource = $resource("/authors");
  var queryAuthors = function(next){
    //use a callback instead of a promise
    authorResource.query({}, function(results){
      var out = [];
      //Underscore's "each" method
      _.each(results,function(result){
          //using our Author prototype above
          out.push(Author(result));
      });
      next(result);
    });
  }
  return {
    authors : queryAuthors
  }
}
app.factory("Api", api);  

A bit more code... but either way we can use it this way:

app.controller("AuthorCtrl", function($scope, Api){

  //returns [{first : 'Mark', last : 'Twain', quote : '...'}]
  Api.authors(function(results){
    $scope.authors = results;
  });

});

Now we have a nice array of Authors. But we've also bent Angular just a bit because we're thinking like Software Developers - expecting "behavior" like "fullName()" to be on our model.

If we take a step back and consider what we're doing here (instrumenting the UI) - that doesn't make all that much sense. Here's where Joel and I will increase our disagreement :) and maybe you'll think I'm a dope.

Domain vs. Presentation Concerns

What is the motive for using the Author prototype? Typically, when working with Single Page Applications it's something that relates to:

  • Code Reuse (Author might be reusable around the app)
  • Centralized Logic (fullName(), daysUntilBirthday())

I'm going to put forward a bit of a strong opinion: if it's not presentation-related, it doesn't belong here. Hopefully we can agree on this as, well, the page is all about presentation and while yes there's logic flying around - it's presentation logic!

Angular provides a better facility for this kind of thing: they're called filters. In Angular we can do this:

var fullNameFilter = function(){  
  return function(person){
    //you can have some logic in here to introspect "person"
    //for "first" or "firstName" or whatever
    return person.first + " " + person.last;
  };
}
//add it to the app
app.filter("fullname", fullNameFilter);  

In your code you can now ask for the full name like this:

<ul>  
  <li ng-repeat="author in authors">{{ author | fullname}}</li>
</ul>  

The really nice thing about this is that you can reuse these filters - in any project! Turning this back to Joel's post - can we solve the problem without introducing a new model/service?

I think so. In looking at the controller code that Joel wants to refactor, we see this:

$scope.list = [twain, fowler, poe, plato];

$scope.selectQuote = function(author) {
    $scope.selectedQuote = author.quote;
    $scope.selectedAuthor = author;
}

$scope.isSelected = function(author) {
    return author === $scope.selectedAuthor;
}

I can guess as to a few reasons why it exists - but I don't think it's going out on a limb to think Joel wants to indicate visually which author the user wants to know more about.

There are a number of ways to solve this - from drop dead simple:

<ul>  
  <li ng-repeat="author in authors">
    <!--select the author, then set the class or whatever is needed. Yay dynamic languages!-->
    <input type='checkbox' ng-model='author.selected'>
    {{fullname | author}}
  </li>
</ul>  

To a more advanced solution involving directives (which I'll sidestep for now - this post is long enough). My ultimate point is that we're in presentation land here and while it's tempting to put your engineering muscle to work, there's just no need.

Blog Logo

Rob Conery

I am the Co-founder of Tekpub.com, Creator of This Developer's Life, an Author, Speaker, and sometimes a little bit opinionated.


Published