AngularJS : How should controllers and factories/services be structured with a rich, hierarchical object model? AngularJS : How should controllers and factories/services be structured with a rich, hierarchical object model? angularjs angularjs

AngularJS : How should controllers and factories/services be structured with a rich, hierarchical object model?


You can use a rich object model, but for objects that are not top-level, their factories should expose an api for creating new instances rather than be used as singletons. This is is somewhat contrary to the design of many apps you see these days, which are more functional than object-oriented--I am not commenting on the pros and cons of either approach, and I don't think Angular forces you to adopt one or the other.

Your example, redesigned, in pseudocode:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {   var inbox = InboxFactory.createInbox();   $scope.getMessages = function(){      inbox.getMessages()           .then(...)   $scope.deleteMessages = function(){      inbox.deleteMessages()           .then(...)});


Your situation becomes much simpler if you adopt a route based approach (a la ngRoute or something similar). Consider this alternative - warning untested code:

app.config(function($routeProvider) {  $routeProvider    .when('/inbox/:inboxId',      templateUrl: 'views/inbox.html',      controller: 'InboxCtrl',      controllerAs: 'inbox',      resolve: {        inboxMessages: function(InboxFactory) {          // Use use :inboxId route param if you need to work with multiple          // inboxes. Taking some libery here, we'll assuming          // `InboxFactory.getMessages()` returns a promise that will resolve to          // an array of messages;          return InboxFactory.getMessages();        }      }    // ... other routes    .otherwise: {      // ...    };});app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) {  var vm = this;  vm.messages = inboxMessages;  vm.openMessage = InboxFactory.openMessage;  vm.deleteMessage = InboxFactory.deleteMessage;});

Look how slim the controller is now! Granted I made use of some more compact syntax in a couple spots but this highlights how our controller really is just glueing things together.

We can further streamline things by getting rid of InboxFactory.messages, when would we actually use it? We're only guaranteed to have to have it be populated after InboxFactory.getMessages resolves, so let's just have this promise resolve to the messages themselves.

Storing data in singletons in this way may be the easiest solution in some cases but it makes life difficult when that data must be fetched on the fly. You're going to be best off leaning on APIs and factories (as AlexMA suggests), pulling down the necessary data whenever a route changes (e.g. the user wants to look at a different inbox) and injecting that data directly into the appropriate controller.

Another benefit of this form is we get to have our data in hand at the time the controller is instantiated. We don't have to juggle asynchronous states or worry about putting lots of code in callbacks. As a corollary, we get to catch data loading errors before displaying a new inbox view and the user doesn't get stuck in a half baked state.

Further to the point of your question though notice that the burden of knowing how your rich model structure fits together is no longer the controller's problem. It just gets some data and exposes a bunch of methods to the view.


After MUCH tinkering and trying different approaches, my final decision is that you shouldn't persist your rich object model across views.

I keep the object model super lean and load just what I need for each view. There is high level data that I keep around (user information like name, id, email, etc. and organization data like which organization they are logged in with), but everything else gets loaded for the current view.

With this lean approach, here's what my factory would look like:

app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {factory.messages = [];factory.openMessage = function (message) {  $location.search('id', message.id).path('/message');};factory.deleteMessage = function (message) {  $http.post('/message/delete', message)  .success(function (data) {    NotificationFactory.showSuccess();    return data;  })  .error(function () {    NotificationFactory.showError();    return null;  });};factory.getMessages = function (userId) {  return $http.get('/messages/user/id/'+userId)  .success(function (data) {    return data;  })  .error(function () {    NotificationFactory.showError();    return null;  });};return factory;});

And the controller:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {  var vm = this;  vm.messages = {};  vm.openMessage = function (message) {    InboxFactory.openMessage(message);  };  vm.deleteMessage = function (message) {    InboxFactory.deleteMessage(message);  };  InboxFactory    .getMessages(userId) //you can get the userId from anywhere you want.    .then(function (data) {      vm.messages = data;  });});

The benefits so far are:

  • simplified application logic
  • lean and mean, lightweight (I only load just what I need for the current state)
  • less memory usage, which translates to overall better performance, especially on mobile