Wednesday, November 26, 2014

The Top 5 Mistakes AngularJS Developers Make Part 1: Relying on $scope

Although AngularJS is an extraordinarily popular framework, there is plenty of discussion and controversy over whether or not it truly adds value to projects. Having witnessed its value firsthand as I described in my recent post, Angular from a Different Angle, I believe it can be a powerful tool when used correctly. Voltaire said, “With great power comes great responsibility.” A tool like Angular can be easily abused. This series is designed to help you avoid common traps and pitfalls before they become a problem.

The top five mistakes I see people make are:

  1. Heavy reliance on $scope (not using controller as) 
  2. Abusing $watch
  3. Overusing $broadcast and $emit
  4. Hacking the DOM
  5. Failing to Test

In this series I’ll cover examples of both how these items are abused and my suggested “better practice.” Let’s get started with the first mistake.

Relying on $scope (Not Using Controller As)

The canonical Angular tutorial inevitably introduces controllers and forces them to take on a dependency to $scope, like this:

<div ng-app="myApp">
    <div ng-controller="badCtrl">
        <input placeholder="Type your name" ng-model="name" />
        <button ng-click="greetMe()" ng-disabled="notValid()">Greet Me!</button>
    </div>
</
div>
(function () {
    var app = angular.module('myApp', []);
    function BadController($scope) {
        $scope.notValid = function () {
            var bad = !!$scope.name;
            return !bad || $scope.name.length < 1;
        };
        $scope.greetMe = function () {
            if ($scope.notValid()) {
                return;
            }
            alert('Hello, ' + $scope.name);
            $scope.name = '';
        };
    }

    app.controller('badCtrl', BadController);
    
})();

A controller in Angular is really what I would refer to as a “view model.” In the diagram below, just replace “XAML” with “HTML” to visualize the relationship.

main[1]

A controller in the traditional sense marshals activity between the application model and view but typically does so in a more active fashion. A view model, on the other hand, simply exposes properties and functions for data-binding. I like this approach because it allows your view models to simply exist as “state bags” – they may interact with services to grab lists or other items but in the end they just expose it. You can then bind to that data and represent it however you like.

Taking on the dependency to $scope does two things. First, it requires the controller to be aware that it is participating in data-binding when it doesn’t have to. It certainly simplifies things when you can just expose a list as a list and a selected item as a property and test that in isolation without worrying about what the glue looks like. The controller defined earlier really just exists as a proxy with the sole purpose of passing setup onto the $scope. Why take on that extra work?

Second, it complicates the testing story because now you have to ensure that a $scope is created and passed in to do something. The controller really becomes a facade to the $scope so you end up with a lot of code marshalling values between the two.

(queue Dr. Evil voice … “If only there were a way for the controller to BE the $scope…”)

Wait! There is! Using the controller as syntax enables you to treat and test controllers as standalone plain old JavaScript objects (POJOs). Consider this example:

<div ng-controller="goodCtrl as ctrl">
    <input placeholder="Type your name" 
            ng-model="ctrl.name" />
    <button ng-click="ctrl.greetMe()" 
            ng-disabled="ctrl.notValid()">Greet Me! </button>
</
div>
(function () {
    var app = angular.module('myApp', []);
    function GoodController() {
    }
    angular.extend(GoodController.prototype, {
        notValid: function () {
            var bad = !!this.name;
            return !bad || name.length < 1;
        },
        greetMe: function () {
            if (this.notValid()) {
                return;
            }
            alert('Hello, ' + this.name);
            this.name = '';
        }
    });
    app.controller('goodCtrl', GoodController);
})();

Although it uses angular.extend, the controller could just as easily have been defined via the constructor function prototype. This makes it a pure JavaScript object. It has no dependencies, and why should it? We haven’t written a controller complicated enough to warrant dependencies yet. $scope just muddies the waters.

I made a fiddle to demonstrate the $scope vs. “controller as” approaches side-by-side.

In fact, if you are taking a test-driven development (TDD) approach, you don’t even have to worry about Angular at first. You can create your controller as a POJO and write plenty of tests, then introduce it as a controller to the application. This provides more flexibility and less reliance on what version is running or even what the UI looks like.

An added bonus is that you can alias the controller in your code so that you either segregate it per section by name, or give it a common name like ctrl to make it easier to build boilerplate HTML code. This syntax is also available from your routes.

Of course, the first response I typically get when I mention not using $scope is “What about watches?” My reply is, “What about them?” Angular does a lot of watching for you, so the real question is whether you really need those watches, or if there is a better way. There may be, and that’s what I’ll cover in the next part of this series.

signature[1]

19 comments:

  1. This is great for simple controllers, but what about when you start adding complexity using $http and/or pretty much any injected service?

    ReplyDelete
  2. "What about it?" Great question! The answer is ... works great! I'll cover more complex controllers in my upcoming posts.

    ReplyDelete
    Replies
    1. The "problem" I've found using "controller as" with controllers with multiple injections is the need to pass them to the controller functions through the "this" property:

      (function () {
      'use strict';

      var MyCtrl = function ($http, fooService, barService) {
      this.$http = $http;
      this.fooService = fooService;
      this.barService = barService;
      };

      MyCtrl.prototype.greetMe = function () {
      this.$http.get('/endpoint').then(...);
      this.fooService(...);
      this.barService(...);
      };

      angular.module('app', []).controller('MyCtrl', MyCtrl);
      })();

      For me, using "$scope" is a bit cleaner:

      angular.module('app', [])
      .controller('MyCtrl', function ($scope, $http, fooService, barService) {
      'use strict';

      $scope.greetMe = function () {
      $http.get('/endpoint').then(...);
      fooService(...);
      barService(...);
      };
      });

      Am I missing something?

      Delete
    2. You're not comparing apples to apples. In the controller as example you used a local reference for dependencies, while with the $scope example you used a captured reference. You don't have to use the prototype in controller as. You can just as easily do Controller($http) { this.greetMe = function() { $http.get ... }; } etc. etc. It's no different than $scope. If you want to reference it outside of the object itself then you have to capture it, it's just the question of whether you take the additional dependency on scope.

      Delete
    3. You are right. I'm using prototype because a I use to do so when defining constructor methods in order to avoid instantiating them with every "class" instance. I assume after your comment that it is not really a greet improvement in angular controllers since they are instantiated at the app startup or on every access to its related view. I don't know when but in both cases the performance/memory hit is affordable

      Delete
  3. I've used it! Prefer Angular, but certainly some things Knockout is great at, such as performance for computed properties.

    ReplyDelete
  4. I've been taking your recommended approach by using the controllers as a VM, but I have found that any time I use ng-repeat, I have to resort to using the $scope because ng-repeat can't find the local controller variable. Am I doing something wrong or is this by design?

    ReplyDelete
    Replies
    1. Not sure what you are referring to. Let's say I have a label and a list on the controller. If I do:

      div ng-controller="myCtrl as ctrl"
      ul li ng-repeat="item in ctrl.list"

      Within the list I can do item.id to get the items id, but I can also do ctrl.label to get the label off the controller. In other words, I have access to both the scope of the current item and the scope of the controller using the aliased name.

      Do you have a fiddle or something to demonstrate your issue? It's not something I've run into.

      Delete
    2. I do! I am new to AngularJS / JS in general, so I may be making a simple mistake. Essentially some other service is updating a variable, and when it is on the scope it auto-updates. I had to use $scope.$apply in the fiddle to demonstrate this with a timeout. http://jsfiddle.net/k2y7xnwe/3/

      Cheers! And great article by the way.

      Delete
    3. Thanks. What you're referring to is less about the controller vs. something external, and more about how digest works. If you have anything external, you need to run it in the context of a digest loop, which in that case would have the root scope. If you are really timing out, use the $timeout service and it will work fine, like this:

      http://jsfiddle.net/jeremylikness/ow5b0ac2/

      If you really need to use a set Timeout, get a function that runs apply for you and set the time out or interval in that context, like this:

      http://jsfiddle.net/jeremylikness/getcygy1/

      Hope that helps.

      Delete
    4. That does make sense -- I put the timeout in to simulate another service updating the variable (waiting on a REST call). Thanks for the help!

      Delete
  5. This whole concept seems weird to me... The role of a controller is to manage the view-model (aka the scope). In this example, there is still a scope created (an anomymous one) for the controller (which is why data binding works), but we are assigning the whole controller instance to it (silently, by binding to one of its properties). This pattern will pollute the scope. I think being explicit about what's added to the scope is more clear... Thoughts?

    ReplyDelete
    Replies
    1. I'm not sure what is weird about the controller being the view model except perhaps naming. What am I polluting? If I have logic in services, the logic stays in the service and not in the controller. Explicitly I know anything I expose as a property or method is part of scope. Even better, instead of relying on some construct ($scope) to do things, I can easily create a POJO as a standalone view model and test it in isolation. Furthermore, the next version won't have a scope for controllers and will lock us into this pattern, so you also receive the benefit of future-proofing your apps. Those are my thoughts, anonymous.

      Delete
  6. how about inherit methods and properties from rootScope or parent scope if using Controller As syntax. In some cases, i want to push some common methods and properties to rootScope or parent scope for those children controller inherit or reuse them ?

    ReplyDelete
    Replies
    1. The question is "why" do you push those common properties. Is it for ease of access? In other words, cat inherits from animal, that makes sense. But does "contact" inherit from "user security context" or is that something artificial you are creating? If you have common properties such as user info, security, time zone, etc., you can easily encapsulate them in a service. Then you can be explicit, i.e. "userSvc.securityParams" rather than relying on hierarchy. The hierarchy is fragile. I've run into projects that depended on the hierarchy and when they were refactored, suddenly things fell apart. If there is a true hierarchical relationship then it makes sense, but in most cases when you are using rootScope as a "session grab bag" you can be better served creating an explicit service that controllers can reference to retrieve those values.

      Delete
    2. Thank you for your advice, the reason is there are some controllers like register, login, account use same methods, so i want to push those to some place for these controllers can use easily, and when i need to change the methods, i just only change in one place. So, my idea is push it in parentScope or rootScope for it children controller can inherit from to archive my goal.

      Delete
    3. That's a perfect use case for a service - the controllers that need the reusable methods can take a dependency on the service that owns those methods and properties.

      Delete
  7. Thank you! This clears up a lot of my initial confusion in learning Angular, which seemed to introduce so many $thingy dependencies/injections to track down and understand (which was increasing my resistance to Angular in general). This is also *perfect* advice in preparing for Angular 2. Each unnecessary $thingy I remove from code I've acquired thru tutorials makes me say "ahhhh" :-)

    ReplyDelete