I read these two great articles:
The state of angularjs controllers by Jonathan Creamer
and
Rethinking AngularJS Controllers by Todd Motto
In these articles, the authors talk about the right way to use controllers (making them anemic bridges between the view and the model) and factories/services (where the business logic should really live).
This is great information, and I was really excited to start refactoring the controllers on one of my projects, but I quickly found that the structure shown in the articles breaks down if you have a rich object model.
Here's a recap of the setup from "Rethinking Angularjs Controllers":
Here's the controller:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) { var vm = this; vm.messages = InboxFactory.messages; vm.openMessage = function (message) { InboxFactory.openMessage(message); }; vm.deleteMessage = function (message) { InboxFactory.deleteMessage(message); }; InboxFactory .getMessages() .then(function () { vm.messages = InboxFactory.messages; }); });
and here's the factory:
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) { factory.messages.splice(index, 1); NotificationFactory.showSuccess(); }) .error(function () { NotificationFactory.showError(); }); }; factory.getMessages = function () { return $http.get('/messages') .success(function (data) { factory.messages = data; }) .error(function () { NotificationFactory.showError(); }); }; return factory; });
This is great and because providers
(the factory) are singletons, the data is maintained across views and can be accessed without having to reload it from the API.
This works just fine if messages
are a top level object. But what happens if they aren't? What if this is an app for browsing the inboxes of other users? Maybe you're an administrator and you want to be able to manage and browse the inboxes of any user. Maybe you need multiple users' inboxes loaded at same time. How does this work? The problem is inbox messages are stored in the service, i.e. InboxFactory.messages
.
What if the hierarchy is like this:
Organization | __________________|____________________ | | | Accounting Human Resources IT | | | ________|_______ _____|______ ______|________ | | | | | | | | | John Mike Sue Tom Joe Brad May Judy Jill | | | | | | | | | Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox
Now messages
are several levels deep in the hierarchy, and have no meaning on their own. You can't store messages in the factory, InboxFactory.messages
because you have to retrieve messages for several users at a time.
Now you will have an OrganizationFactory, a DepartmentFactory, a UserFactory, and an InboxFactory. Retrieving "messages" must be in the context of a user
, who is in the context of a department
, which is in the context of an organization
. How and where should the data be stored? How should it be retreived?
So how should this be resolved? How should controllers, factories/services, and rich object models be structured?
At this point in my thinking, I'm leaning towards just keeping it lean and not having a rich object model. Just store the objects on the $scope injected into the controller, and if you navigate to a new view, reload from the API. If you need some data persisted across views, you can build that bridge with a service or factory, but it shouldn't be the way you do most things.
How have other's solved this? Are there any patterns out there for this?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With