Angular Icon Get 73% off the Angular Master bundle

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs

Write AngularJS like a pro. Angularjs Icon

Follow the ultimate AngularJS roadmap.

Rethinking AngularJS Controllers

Note: please don’t use this approach anymore, use Components and one-way dataflow

Last month Jonathan Creamer wrote an awesome article on Angular and MVC, The state of AngularJS Controllers. The article touches on misconceptions of client-side MVC and true Model and Controller separation, as well as things we often do wrong by incorrectly using a Controller as a Model.

This article is my rethinking of Angular Controllers, and best practices when using them in your team or for yourself. Getting and setting data outside of the Controller, resolving data is key to using a Model correctly.

Controller as Model

What we’re used to seeing in Controllers is most likely this (Angular docs even show us everything using this approach):

app.controller('InboxCtrl',
  function InboxCtrl ($scope, $location, NotificationFactory) {

  $scope.messages = [];

  $scope.openMessage = function (message) {
    // myapp.com/message?id=19286396
    $location.search('id', message.id).path('/message');
  };

  $scope.deleteMessage = function (message) {
    $http.post('/message/delete', message)
    .success(function () {
      $scope.messages.splice(index, 1);
      NotificationFactory.showSuccess();
    });
  };

  $http.get('/messages')
  .success(function (data) {
    $scope.messages = data;
    NotificationFactory.showSuccess();
  })
  .error(function (data) {
    NotificationFactory.showError();
  });

});

This example totally abuses the Controller, almost treating it as a logic provider as well as the Model itself. We’re using it to hold, create and maintain the Model state as well as littering our Factories and other Services. This has massive impact across many things:

This results in the Controller becoming a bloated playground which can quickly spiral out of control into an intangible mess.

Abstracting the Model

Jonathan’s post made it strictly clear that absolutely no Model data should be created or persisted in the Controller. I totally second that. When I first started writing Angular applications, I began thinking “What is the best way to create reusable functions?”. I’m pretty settled on the following…

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

Reworking our Controller, we could aim to completely kill all Model creations in the Controller:

app.controller('InboxCtrl',
  function InboxCtrl ($scope, InboxFactory) {

  $scope.messages = InboxFactory.messages;

  $scope.openMessage = function (message) {
    InboxFactory.openMessage(message);
  };

  $scope.deleteMessage = function (message) {
    InboxFactory.deleteMessage(message);
  };

  InboxFactory
    .getMessages()
    .then(function () {
      $scope.messages = InboxFactory.messages;
    });

});

+1 for readability, +1 for simplicity and +1 for being so easy to reuse. I’ve included an example of using a promise to resolve asynchronously fetched data as well.

We of course need the Factory to go with it:

app.factory('InboxFactory',
  function InboxFactory ($location, NotificationFactory) {

  InboxFactory.messages = [];

  InboxFactory.openMessage = function (message) {
    $location.search('id', message.id).path('/message');
  };

  InboxFactory.deleteMessage = function (message) {
    $http.post('/message/delete', message)
    .success(function (data) {
      InboxFactory.messages.splice(index, 1);
      NotificationFactory.showSuccess();
    })
    .error(function () {
      NotificationFactory.showError();
    });
  };

  InboxFactory.getMessages = function () {
    return $http.get('/messages')
    .success(function (data) {
      // magic line, we resolve the data IN the factory!
      InboxFactory.messages = data;
    })
    .error(function () {
      NotificationFactory.showError();
    });
  };

});

Binding into our Controllers just got a lot simpler, the Model is abstracted, as is the Controller.

Resolving Model data, no callback arg binding in Controllers

You may have noticed from the above that I did this:

InboxFactory
  .getMessages()
  .then(function () {
    $scope.messages = InboxFactory.messages;
  });

We might be used to something like this:

InboxFactory
  .getMessages()
  .then(function (data) {
    $scope.messages = data;
  });

Which at first looks great, but breaks the golden rule of maintaining state in the Controller (as I’ve referenced data from the promise argument). The trick to this is resolving the data inside the Factory itself, which allows it to manage itself and the Controller simply be told it’s updated:

InboxFactory.getMessages = function () {
  return $http.get('/messages')
  .success(function (data) {
    // magic line, we resolve the data IN the factory!
    InboxFactory.messages = data;
  })
  .error(function () {
    NotificationFactory.showError();
  });

At this point, the $http.get() is returned, and inside our Controller we can call .then() again and know our data has been resolved and bound to the Factory. We only reference the updated value in the Controller in the callback.

“Controller as” syntax

I recently wrote about the “Controller as” syntax in Angular, and combined with the new MVC approach things just hit the next level of awesome inside Controllers:

// <div></div>
app.controller('InboxCtrl',
  function InboxCtrl (InboxFactory) {

  this.messages = InboxFactory.messages;

  this.openMessage = function (message) {
    InboxFactory.openMessage(message);
  };

  this.deleteMessage = function (message) {
    InboxFactory.deleteMessage(message);
  };

  InboxFactory
    .getMessages()
    .then(function () {
    this.messages = InboxFactory.messages;
  }.bind(this)); // use angular.bind for < IE9

});

John Papa left an interesting comment on another article of mine, where he uses var vm; as a descriptor inside the Controller, meaning “View Model”. His suggestion was to use the following, which also avoids needing to use .bind():

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;
  });

});

What is the Model?

I’ve been chatting with Jonathan recently on what “is” the Model in client-side MVC. Is it the data? Is it the way to get the data? Is it both? Is it a way to persist the data?

We kind of agreed on a mix of both and that there are many options and opinions. We also agreed that using Controller as mixed with the above approach is superb architecture for Angular. The word “Model” might not be completely correct on what I’m describing above, business logic might be another word, but to keep things easy I’ve stuck with “Model”.

Jasmine/Karma unit testing is made much easier as well, we can test the Factory to ensure it’s hitting all its endpoints, fetching and updating the local data - and when testing the Controller we can go in knowing our Factory is bulletproof which will help us track down errors faster, and make our Controller tests even slimmer.

Using $scope

If you need to use $scope, you’ll likely need to listen to an event or emit one, or $watch for Model changes. Based on the above, this isn’t a good idea to tie Model behaviour into the Controller again. Using Controller as allows us to inject the $scope, but think about how you can abstract the same behaviour into a Factory.

The $scope Object can be dependency injected into the Controller and used if needed.

Rules for the Controller going forward:

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover