Slide 1

Slide 1 text

Controllers Patterns for successful use

Slide 2

Slide 2 text

History To understand controllers and the confusion around controllers we need to have a look at their history. Before Angular 1.0, they were just …

Slide 3

Slide 3 text

Global Functions …any global function or function on scope, that got executed by the ng-controller directive or by a route to…

Slide 4

Slide 4 text

Initializing Scope …initialize that part of the scope.

Slide 5

Slide 5 text

Separate Instances The controllers we have now are instantiated objects, the controller functions are actually constructors for these objects and not just initializers. ! To start thinking about controllers, let me ask you a question: …

Slide 6

Slide 6 text

What do you want on your Scope? What kind of stuff do you want to put on your scope? Let me also give you the answer: …

Slide 7

Slide 7 text

As little as possible As little as possible Why? You don't want to rely on scope too much to provide the context for your calculations. Using the scope too much is just as bad as relying on global vars.

Slide 8

Slide 8 text

So: So, only two things basically

Slide 9

Slide 9 text

Functions What about functions that you want to bind to, that have to perform the action your users want to perform? This one for example…

Slide 10

Slide 10 text

$scope.addUser = function(group, user){ return user.setGroupId(group.id) .then(function(){ return group.reload() }) .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.

Slide 11

Slide 11 text

Function State If these functions have maintain some state, they have to store that state on the scope as well.

Slide 12

Slide 12 text

$scope.inProgress = false $scope.addUser = function(group, user){ if ($scope.inProgress) return $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?

Slide 13

Slide 13 text

“Workers” Small, stateful objects with functions Let’s put these functions 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

Slide 14

Slide 14 text

function GroupWorker(){ this._inProgress = false } ! GroupWorker.prototype.addUser = function(group, 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: …

Slide 15

Slide 15 text

Add {{user.name}} to {{group.name}} - Now, wherever you see that addUser method, you know where it’s coming from. - all groupworker methods and their state is hidden away inside the worker

Slide 16

Slide 16 text

What goes in a “worker”? Workers should contain methods and caches needed in a context of your app that are not directly attached/part of your models. The addUser function in our example

Slide 17

Slide 17 text

$scope.inProgress = false $scope.addUser = function(group, user){ if ($scope.inProgress) return $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

Slide 18

Slide 18 text

Defining “workers” How do you declare a worker? You could do the following…

Slide 19

Slide 19 text

myModule.service(‘GroupWorker’, function(){…}) ! myModule.directive(‘group’, function(GroupWorker) { return { ... link: 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.

Slide 20

Slide 20 text

You probably guessed it, these are actually not called workers…

Slide 21

Slide 21 text

Workers
 Controllers! These are Controllers! ! So instead of declaring your workers like shown you will write code like…

Slide 22

Slide 22 text

function groupDirective() { return { ... controller: ‘GroupController', controllerAs: ‘groupController’, link: function(scope, element, attributes) { ... } } } this ! and Angular will take care of …

Slide 23

Slide 23 text

Instantiation … all the Instantiation automatically and allow you to …

Slide 24

Slide 24 text

Context Services … inject Context and Services into the Controller instances …

Slide 25

Slide 25 text

function GroupController($scope, notificationService){ ... } … by listing them as Parameters to the constructor function.

Slide 26

Slide 26 text

Controller on the Scope Angular also takes care of exposing the controller on the Scope for you…

Slide 27

Slide 27 text

... controller: ‘GroupController', controllerAs: ‘groupController’, ... ! $scope.groupController.addUser( selectedUser, selectedGroup) … as whatever you entered in the ‘controllerAs’ configuration parameter.

Slide 28

Slide 28 text

Requiring Controllers Let’s look at the few differnt ways to make Angular instantiate Controllers. There are three

Slide 29

Slide 29 text

directive(‘foo’, function(){ return { controller: ‘FooController’, controllerAs: ‘fooController’, ... } }) 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.

Slide 30

Slide 30 text

ngController Second, the ngController-Directive is 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

Slide 31

Slide 31 text

$routeProvider .when(‘/Book/:bookId', { controller: ‘BookController’, controllerAs: ‘bookController’ ... }) Routes Finally, if you use Routes to navigate to views, the controller parameter in your route definition can instantiate a controller for that route.

Slide 32

Slide 32 text

Initializing Scope Don’t forget, Controllers still run their constructors and 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.

Slide 33

Slide 33 text

Coding Style It helps to have a pattern for code layout to keep your controllers clean

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Advanced Patterns Let us look at advance usage patterns For them we need the $controller injector.

Slide 41

Slide 41 text

$controller … $controller is a special injector, used to instantiate controllers. You can use it for two interesting things: …

Slide 42

Slide 42 text

Inheritance First: Inheritance

Slide 43

Slide 43 text

function AbstractController($scope, who) { this._who = who this.greet = function(){ 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

Slide 44

Slide 44 text

Assembly Generally you should favor assembly over inheritance. Assembly means that you simply instantiate other, special purpose controllers, allowing them to cooperate by

Slide 45

Slide 45 text

• Call methods • Send events • Watch $scope calling each others methods sending events and watching the scope

Slide 46

Slide 46 text

function MainController($scope, $controller){ ! $scope.listController = $controller(‘ListController', {$scope: $scope}) $scope.itemController = $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.

Slide 47

Slide 47 text

function ComplexController($scope, $controller){ ! var listController =
 $controller('ListController', {$scope: $scope}); ! var itemController =
 $controller('ItemController', {$scope: $scope}); this.clearLists =
 listController.clearLists.bind(listController); ! this.clearItems = itemController.clearItems.bind(itemController); } ! ! 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.

Slide 48

Slide 48 text

Testing All these Patterns help you test your controllers and 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.

Slide 49

Slide 49 text

Recap • Build controller instances • Keep your scope clean • Keep your controllers small: • Single Purpose, small API • Assemble and inherit

Slide 50

Slide 50 text

Thanks! [email protected] @agento Btw: We’re hiring