AngularJS with CoffeeScript avoiding scope hell

09 Jan 2016

In Angular 1.x, think of $scope methods as public methods.

As usual, it’s best to expose as little as possible of your class to the outside world. In Angular 1.x, regular methods in your directive are not accessible to the view layer.

This means we can think of $scope methods as public methods and regular methods in the directive as private methods. You want few $scope methods and many “private” functions in your directive.

Like in any other language, every if statement is a potential method to be extracted.

This is the typical way I keep my directives clean.

# angular/directives/user_settings_store_directive.coffee
@app.directive 'userSettings', ['Users', 'UserDefault', (Users, UserDefault) ->
  restrict: 'E'
  templateUrl: 'settings/user_settings.html'
  controllerAs: 'userSettingsCtrl'
  controller: ['$scope', ($scope) ->

    # Set up user data
    setup_user_data = (raw_users) ->
      $scope.users = raw_users
      user.name = "#{user.first_name} (#{user.last_name})" for user in $scope.users

    # calls API: GET /users.json
    fetch_user_list = -> Users.get().$promise.then ((result) -> setup_user_data(result) )

    # calls API: PUT /users/:user_id/default.json
    update_default = (user) ->
      UserDefault.update(user_id: user.id).$promise.then ( -> fetch_user_list() )

    users = -> $scope.users.length > 0

    $scope.users = []
    fetch_user_list()

    $scope.users = -> users()
    $scope.update_default = (user) -> update_default(user)

    return
  ]
]

The HTML for this directive would look something like this.

# templates/settings/user_settings.html
<div class="row">

    <h5 class="center-align">Set default user</h5>
    <div ng-if="!uses()">
        Unable to find users
    </div>

    <div ng-if="users()">
        <strong>Users</strong>
        <p ng-repeat="user in users">
            <input type="checkbox"
                   ng-checked="user.default"
                   ng-click="update_default(user)"
                   id="user-" />
            <label for="user-"></label>
        </p>
    </div>

</div>

For those who prefer JavaScript to CoffeeScript, this the compiled JavaScript. Below is the same directive as the CoffeeScript above.

this.app.directive('userSettings', [
  'Users', 'UserDefault', function(Users, UserDefault) {
    return {
      restrict: 'E',
      templateUrl: 'settings/user_settings.html',
      controllerAs: 'userSettingsCtrl',
      controller: [
        '$scope', function($scope) {
          var fetch_user_list, setup_user_data, update_default, users;
          
          setup_user_data = function(raw_users) {
            var i, len, ref, results, user;
            
            $scope.users = raw_users;
            
            ref = $scope.users;
            
            results = [];
            
            for (i = 0, len = ref.length; i < len; i++) {
              user = ref[i];
              results.push(user.name = user.first_name + " (" + user.last_name + ")");
            }
            return results;
          };
          
          fetch_user_list = function() {
            return Users.get().$promise.then((function(result) {
              return setup_user_data(result);
            }));
          };
          
          update_default = function(user) {
            return UserDefault.update({
              user_id: user.id
            }).$promise.then((function() {
              return fetch_user_list();
            }));
          };
          
          users = function() {
            return $scope.users.length > 0;
          };
          
          $scope.users = [];
          
          fetch_user_list();
          
          $scope.users = function() {
            return users();
          };
          
          $scope.update_default = function(user) {
            return update_default(user);
          };
        }
      ]
    };
  }
]);