This post summarizes a talk I gave at the AngularJS Meetup Berlin in October 2014.
Controllers are are a vital but widely misunderstood part of AngularJS. Thousands of clueless articles and very unfortunate examples in the official documentation are helping to spread confusion instead of clarity about the proper use of controllers.
I want to shed some light on the issue and present how we use controllers successfully in the Contentful user interface.
To understand controllers and the confusion around them, we need to have a look at their history.
Before Angular 1.0, controllers were just global functions, or functions on the scope, that got executed by either the ngController directive or the router to initialize their part of the scope.
These days, controllers are actually instantiated objects, with the Controller functions being the constructors for these objects, instead of simple initializers for the scope.
What do you want on your scope?
To keep an Angular app maintainable, you should avoid stuffing a lot of properties onto your scope. You don’t want scope properties to provide too much of the input to your functions. Using the scope this way is almost as bad as using global variables: It’s very hard to understand where all these values are coming from and under which circumstances they might change.
So, you try to restrict yourself to two kinds of things on your scope:
- Model objects
- Handlers that you bind to events on your DOM nodes, allowing users to perform actions inside your app.
Such a function might just look like this:
These functions seem harmless but they are actually problematic and you can end up with quite a lot of them on your scope. You will end up having these functions everywhere, without knowing where they’re coming from.
If a function has to maintain some state, it needs to store that state on the scope as well:
This variant of the function prevents multiple simultaneous requests by storing the
inProgress variable on the scope. Great, even more stuff on the scope! The name
inProgress is pretty generic, someone else could easily come along, use the same property name for something different and create bugs that can be very hard to track down.
How can you prevent this?
You could put these functions into small stateful objects, that group related functions together and also store their state. Let’s see how the example might look when we do that:
Now our function is attached to the worker and also stores its state inside the worker. We’re not polluting the scope and nobody can accidentally mess with the progress flag.
Whenever you need a worker, you can instantiate one and call their methods like this:
Workers should contain all the methods and state you need in the context of your app, that are not directly attached to or part of your models. The
addUser function from the example, used to add users to groups, can’t really go into either of these models because it has a dependency on the
Notification service. Models should not have dependencies on services that are not related to their actual purpose. So,
addUser is a perfect candidate for an action that you define in a GroupWorker, together with all the other methods related to users and/or groups.
How do you define a Worker? You could define GroupWorkers by yourself, using a combination of services to store the worker’s constructor, and link functions to create the instances:
But Angular already has a facility for declaring workers and makes this much easier for you. You probably guessed it already: What I called “Workers” until now are actually Controllers.
The code you’d actually write in AngularJS to define </strike>workers</strike> Controllers will be this instead:
Now Angular will take care of creating an instance of our GroupController and exposing it on the scope as
groupController. Instantiating controllers also allows you to reference services in the constructor parameters through AngularJS dependency injection mechanism:
There are three different ways of getting controllers into your app. The primary one is through custom directives as seen in the last example:
The legacy way is to use the ngController directive inside your templates to instantiate a Controller:
You should really try to avoid this one. Directive are the primary way to divide your apps into independent components and their respective controllers should be responsible for everything inside each component defined those directives.
Sprinkling controllers randomly through the templates with ngController should never be necessary if your app is modular enough. If you ever find yourself in a situation where you think about using ngController, ask yourself if the component you’re working on has too many responsibilities, is too big or too complex.
The third way is to attach a controller to a route:
If your app follows a more page-centric architecture and you use Routes to navigate views, the
controllerAs parameters in your route definition will instantiate a controller for that route on your view.
Initializing models on the scope
While a Controller’s constructor function is mostly used to set up the controller instance itself, it is also responsible for loading models into the scope, setting up watchers and event handlers.
Any code that 1. does not touch the DOM (use link functions for that) 2. needs to run in multiple places in your app (use services for singletons)
should go into a controller. To keep an overview about the functionality inside your Controller, you can follow a few simple rules that help to keep Controllers clean.
Code Organization for Controllers
First, take a look at this controller, I’ll add the explanations below.
Let’s go through this, step by step.
this can’t be used. Instead of
this, the callbacks can refer to
controller through their closure to get a reference to the controller instance.
I much prefer to only depend on locals and the $injector service in the parameters for a service or Controller, loading the rest of the services through
$injector.get(). This has three advantages:
- The parameter list for the function doesn’t become too long. You avoid line breaks in the parameter list and make it easier to read.
- It’s easier to keep the parameters and the Array with the service names for the dependencey injection in sync if both are short.
- When you use JSHint to warn about unused variables and parameters, this pattern is more reliable than a parameter list. JSHint can only warn about unused parameters at the end of the list, not in the middle, but will always warn about an unused variable.
3. Scope stuff
There are three different things you can do with the scope:
- Set up watchers
- Expose methods or data
- Listen to events
Keep statements for each of these categories together and stick to the rule of having one line per statement. Event handler or watch functions should not be defined inline, but declared further down in the file and referenced here by name.
The “one line per statement” rule keeps the upper part of the controllers neat and tidy and makes it possible to fit all relevant information on one screen.
4. Controller properties
If you want to expose properties and methods on the controller, keep these statements together, just as you did with the scope statements.
5. Clean up
This section is usually not necessary in controllers. But you can use the same rules laid out here to structure link functions. In those you might want to clean up a bit when their part of your the app is being destroyed.
Finally, write down all the definitions for the functions you have referenced so far. Everything from here on is internal to the controller. The external interfaces, everything a user of the controller could interact with, has been defined above this part and can be glanced over quickly.
Advanced Controller patterns
There are two advanced usage patterns I came up with for working with complex controllers. Both make use of the
$controller service. Like other services,
$controller can be retrieved through the
$controllrer service is itself an injector, albeit a special one: it returns new instances of Controllers. It is invoked with the name of a controller and an object containing so called
locals. Locals are injected into the Controller through the standard dependency injection mechanisms. The locals usually passed into a Controller are
IMPORTANT NOTICE: Due to changes in Angular 1.3, the inheritance pattern described here does not work anymore under most circumstances.
The first pattern is Inheritance. AngularJS performs a complicated process to instantiate controllers. This is done to make dependency injection work and looks like this (simplified pseudocode):
Here we are exploiting that an object returned from a constructor will become the return value of the constructor invocation. This is ensured even by the convoluted mechanism AngularJS uses for the instantiation.
What happens in effect is that a caller attempting to instantiate the ConcreteController, actually an instance of the AbstractController, with its missing parts filled by providing them as locals (
who in the example).
- Calling each other’s methods,
- Sending events, or
- Watching the scope
This is an example where our main controller instantiates two special purpose controllers and puts them on the scope:
But Assembly even makes sense if you don’t want to expose the smaller controllers. You can use assembly to break up your controllers into several smaller ones simply because they became too large or if you want to share some behavior between different parts of your app (A ListController, that does sorting and filtering might be used in many places). Just store the smaller controllers in local variables (or not at all) and selectively make their methods accessible from the outside if you need to expose them:
To make the best use of Controllers in AngularJS
- Build and work with controller instances
- Keep your scope clean
- Keep your controllers small.
- Give then a single purpose and a small API
- Use assembly and inheritance to break up controllers that become too large