the controller functions are actually constructors for these objects and not just initializers. ! To start thinking about controllers, let me ask you a question: …
.then(function(){ return Notification.success("Added successfully") }, function(){ Notification.fail("There was a problem") return $q.reject() }) } This is problematic, you can end up with a lot of them on the scope. In the end you have all these functions there and you don’t know where they’re coming from.
$q.reject() $scope.inProgress = true return user.setGroupId(group.id) .then(function(){ return group.reload() }) .then(function(){ return Notification.success(...) $scope.inProgress = false }) } This variant of the function prevents multiple requests by storing a variable on the scope. That’s bad, now we have even more stuff on the scope. And someone else might reuse the same propertyName and create bugs. ! How can we solve that?
into service workers. Small stateful objects that can hold these functions + their state and group them together by topic. Let’s look at the example again
user){ var self = this if (this._inProgress) return $q.reject(“In Progress”) this.inProgress = true return this._doTheStuff() .finally(function(){ self._inProgress = false }) } ! $scope.groupWorker = new GroupWorker() Now our function is attached to the worker and also stores its state on the worker. We’re not polluting the scope and nobody can accidentally mess with the progress flag. ! So, we can instatiate workers whenever we need one and call their methods like this: …
$q.reject() $scope.inProgress = true return user.setGroupId(group.id) .then(function(){ return group.reload() }) .then(function(){ return Notification.success(...) $scope.inProgress = false }) } You don't really want that on your user or your group because of the NotificationService. And actually, these methods are pretty common and you probably want a lot of them
function(scope, element, attributes) { scope.groupWorker = new GroupWorker(); ... } ... } }) …and define GroupWorkers yourself. ! But Angular already has a facility for declaring workers and makes this easier for us.
}) Custom Directive First, when you are writing custom directives, adding the Controllers here is the most basic way. Use controllers in directives to control all the logic that belongs to a directive.
kind of a leftover from ancient times. Use it when there’s a point in your application where you want to introduce functionality, but it doesn’t make sense to have a directive. ! Try to avoid it
have access to the scope. They still perform initialization, set up watchers, event handlers and properties. And as long as the DOM is not touched, this kind of initialization belongs into controllers, not in link functions.
var controller = this; // 2. requirements var editingInterfaces = $injector.get('editingInterfaces'); var logger = $injector.get('logger'); // 3. Do scope stuff // 3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); As the first statement you might want to create a this reference. You’ll need that as soon as you deal with callbacks or promises.
var controller = this; // 2. requirements var editingInterfaces = $injector.get('editingInterfaces'); var logger = $injector.get('logger'); // 3. Do scope stuff // 3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; ! // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); use strict'; angular.module(‘contentful').controller('FormWidgetsController', ['$scope', ‘$injector’, function($scope, $injector){ // 1. Self-reference var controller = this; // 2. requirements var editingInterfaces = $injector.get('editingInterfaces'); var logger = $injector.get('logger'); // 3. Do scope stuff // 3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); Then write your requirements ! Injector scales better if you have more dependencies: - Unused services/jshint - List of parameters does not get too long
var controller = this; // 2. requirements var editingInterfaces = $injector.get('editingInterfaces'); var logger = $injector.get('logger'); // 3. Do scope stuff // 3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); Next: Set up all your interactions with the scope: - Watchers - Exposing methods and data - Event Handlers The order inside this block doesn’t really matter, but try to keep elements together
3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); // 6. All the actual implementations go here. function updateWidgets() { … } function updateWidgetsFromInterface(interf) { … } }]); The Controller itself will also have properties and methods ! Generally write stuff in steps 3 and 4 on single lines, and put the function definitions further down. That helps you to keep an overview, see all interfaces in one glance
3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); // 6. All the actual implementations go here. function updateWidgets() { … } function updateWidgetsFromInterface(interf) { … } }]); Finally, I add the cleanup last. Ususally controllers won’t need this, link functions do and this structure is also useful for structuring link functions
3a. Set up watchers on the scope. $scope.$watch(localesWatcher, updateWidgets, true); $scope.$watch('spaceContext.space.getDefaultLocale()', updateWidgets); $scope.$watch('preferences.showDisabledFields', updateWidgets); $scope.$watch('errorPaths', updateWidgets); // 3b. Expose methods or data on the scope $scope.widgets = []; // 3c. Listen to events on the scope // 4. Expose methods and properties on the controller instance this.updateWidgets = updateWidgets; this.updateWidgetsFromInterface = updateWidgetsFromInterface; // 5. Clean up $scope.$on('$destroy', function(){ // Do whatever cleanup might be necessary controller = null; // MEMLEAK FIX $scope = null; // MEMLEAK FIX }); // 6. All the actual implementations go here. function updateWidgets() { … } function updateWidgetsFromInterface(interf) { … } }]); At the bottom of the file I finally put all my method implementations. ! ! If you do this, you can keep the interesting part of your controller on one screen height and immediately see what it does without having to read all the functions.
return “Hello” + this._who } } ! function ConcreteController($scope, $controller) { return $controller('AbstractController', { $scope: $scope, who: 'World', }) } Calling the ConcreteController constructor will return an instance of the abstract controllers that has been made concrete, by passing in the missing parts as locals. ! The second pattern is Assembly
= $controller('ItemController', {$scope: $scope}) //...do other stuff... } This is an example for assembly: In this case 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.
! var itemController = $controller('ItemController', {$scope: $scope}); this.clearLists = listController.clearLists.bind(listController); ! this.clearItems = itemController.clearItems.bind(itemController); } ! ! <button ng-click="complexController.clearLists()"> You might want to just break up your controllers into several smaller ones because they became too large. ! In this case store the smaller controllers in local variables and selectively make their methods accessible from the outside.
I urge you to write them test-driven. Remember: TDD is a design technique. Following it, with the knowledge from this talk will guide you to a nice architecture.