Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Controllers - Patterns or successful use

Jan
October 08, 2014

Controllers - Patterns or successful use

Jan

October 08, 2014
Tweet

Other Decks in Programming

Transcript

  1. 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 …
  2. Global Functions …any global function or function on scope, that

    got executed by the ng-controller directive or by a route to…
  3. 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: …
  4. 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: …
  5. 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.
  6. 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…
  7. $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.
  8. Function State If these functions have maintain some state, they

    have to store that state on the scope as well.
  9. $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?
  10. “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
  11. 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: …
  12. <button ng-click=“groupWorker.addUser(group, user)"> Add {{user.name}} to {{group.name}} </button> - 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
  13. 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
  14. $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
  15. 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.
  16. Workers
 Controllers! These are Controllers! ! So instead of declaring

    your workers like shown you will write code like…
  17. function groupDirective() { return { ... controller: ‘GroupController', controllerAs: ‘groupController’,

    link: function(scope, element, attributes) { ... } } } this ! and Angular will take care of …
  18. Controller on the Scope Angular also takes care of exposing

    the controller on the Scope for you…
  19. Requiring Controllers Let’s look at the few differnt ways to

    make Angular instantiate Controllers. There are three
  20. 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.
  21. <div ng-controller= “FooController as fooController”> 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
  22. $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.
  23. 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.
  24. Coding Style It helps to have a pattern for code

    layout to keep your controllers clean
  25. 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.
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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.
  31. Advanced Patterns Let us look at advance usage patterns For

    them we need the $controller injector.
  32. $controller … $controller is a special injector, used to instantiate

    controllers. You can use it for two interesting things: …
  33. 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
  34. Assembly Generally you should favor assembly over inheritance. Assembly means

    that you simply instantiate other, special purpose controllers, allowing them to cooperate by
  35. • Call methods • Send events • Watch $scope calling

    each others methods sending events and watching the scope
  36. 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.
  37. 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); } ! ! <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.
  38. 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.
  39. Recap • Build controller instances • Keep your scope clean

    • Keep your controllers small: • Single Purpose, small API • Assemble and inherit