Cleaner class-based controllers for AngularJS

Structure

As your Angular controller grows, it can start to get unwieldy and difficult to structure.

Classy uses a class-based approach and steals some neat ideas from AngularDart to make controllers a little nicer.

The code to the right is a Classy controller based on the TodoMVC project. Take a look, Classy keeps your controllers clean and structured.

Hover your cursor over the code on the right for comments and explanations.


No more annotating your dependencies

This is a big-ee. Angular veterans will know that if you want your Angular code to work with minifiers then you have to annotate your dependencies (i.e. list your dependencies twice), like so (without Classy):

app.controller 'AppCtrl',
  ['$scope', '$location', '$http',
  ($scope, $location, $http) ->
    # ...
]
app.controller('AppCtrl',
  ['$scope', '$location', '$http',
  function($scope, $location, $http) {
    // ...
}]);

Annotating your dependencies is annoying and also it's just not very DRY. If you want to add/remove a dependancy then you need to remember to do it in two places.

In Classy you don't need to do that, it works with minifiers and your code remains DRY. Here's what it looks like with Classy:

app.classy.controller
  name: 'AppCtrl'
  inject: ['$scope', '$location', '$http']
  # ...
app.classy.controller({
  name: 'AppCtrl',
  inject: ['$scope', '$location', '$http'],
  // ...
});

$scope convenience

Functions are automatically added to the controller's $scope

Most of the time when you add a function to a controller, you want it available on the $scope. This is so that you can easily call it in your html using directives like ng-click. Here's how it used to look without Classy:

$scope.editTodo = (todo) -> #...
$scope.editTodo = function(todo) {
  //...
}

and now with Classy:

editTodo: (todo) -> #...
editTodo: function(todo) {
  //...
}

If you don't want the function to be on the $scope then just prefix it with an underscore character (_).

Handy shortcut for $scope

To access the $scope You can simply write @$.foo = 'bar'this.$.foo = 'bar'; instead of @$scope.foo = 'bar'this.$scope.foo = 'bar';. Although you can use still use this.$scope if you prefer.


Special object for $watch listeners

Instead of polluting your init method with lots of calls to $scope.$watch, you can put them in a watch object instead:

watch:
  'location.path()': (newValue, oldValue) ->
    # ...

  '{object}todos': (newValue, oldValue) ->
    # ...
watch: {
  'location.path()': function(newValue, oldValue) {
    // ...
  },
  '{object}todos': function (newValue, oldValue) {
    // ...
  }
}

Notice the {object} keyword in the second listener above. This allows you to easily specify the type of watcher to use. This is much more explicit than Angular's approach. Here is a table of the available keywords:

Keyword $watch Type
{collection} or {shallow} $watchCollection(..)
{object} or {deep} $watch(.., objectEquality = true)

Only 1KB (gzipped and minified)

it's super tiny so you don't have to worry about it adding weight to your application.


Bonus beta features

Named dependencies

Classy allows you to name dependencies whatever you like, simply pass in an object instead of an array. Use a '.' value if you wish to use the original name for a dependency.

app.classy.controller
  name: 'MyCtrl'
  inject:
    $scope: '$'
    filterFilter: 'filter'
    $location: '.'

  init: ->
    # Check if dependencies are defined
    console.log @$ # ✔ ChildScope {}
    console.log @$scope # ✘ undefined
    console.log @$location # ✔ LocationHashbangUrl {}

    # Use a dependency
    console.log @filter(@$.todos, completed: true)
    # -> [{"title":"Learn Angular","completed":true}]
app.classy.controller({
  name: 'MyCtrl',
  inject: {
    $scope: '$',
    filterFilter: 'filter',
    $location: '.'
  },
  init: function() {
    // Check if dependencies are defined
    console.log(this.$); // ✔ ChildScope {}
    console.log(this.$scope); // ✘ undefined
    console.log(this.$location); // ✔ LocationHashbangUrl {}

    // Use a dependency
    console.log(this.filter(
      this.$.todos,
      { completed: true }
    )); // [{"title":"Learn Angular","completed":true}]
  }
});

Reverse-reference controllers

This feature is stolen from AngularDart (you may also be familiar with it from Backbone and other MVC frameworks). It's best explained with code.

This is how you would typically bind a controller to a view with Angular Classy:

<!-- In your HTML -->
<div id="footer" ng-controller="FooterCtrl"></div>

# In your Coffeescript
app.classy.controller
  name: 'FooterCtrl'
  #...
<!-- In your HTML -->
<div id="footer" ng-controller="FooterCtrl"></div>

// In your JS
app.classy.controller({
  name: 'FooterCtrl',
  //...
});

If you want to use reverse-reference controllers then you simply give your controller an element selector reference instead of a name, like this:

<!-- In your HTML -->
<div id="footer"></div>

# In your Coffeescript
app.classy.controller
  el: '#footer'
  #...
<!-- In your HTML -->
<div id="footer"></div>

// In your JS
app.classy.controller({
  el: '#footer',
  //...
});

Classy will use jQuery as it's selector engine if available, otherwise it will fallback to document.querySelectorAll. There is currently no test coverage for this feature so it's probably best not to use it in production (unless you're a badass).


FAQs

Click the questions below to expand the answers.

How do I use Classy Controllers in a directive?

You use them the same way you normally would, except you don't need to give the controller a name because the controller does not need to be registered outside of Angular.

app.directive('classyDirective', function() {
  return {
    controller: app.classy.controller({
      inject: ['$scope'],
      init: function() {
        this.$.testing = 'worked';
      }
    })
  };
});

How do I reference a Classy controller in a route?

Classy controllers are registered just like normal controllers in Angular so you can reference them the same way (it works in ui-router too).

.when('/classy', {
  controller: 'myClassyController',
  templateUrl: 'classy.html'
});

How do I use classy with the `TodoCtrl as todo` syntax?

Angular Classy works with controllerAs out-of-the-box, you don't need to do anything. If you want to make things a bit cleaner you can prevent public functions from being added to the $scope by simply changing the config. You can do this on a per-module basis:

app.classy.options.controller = {
    addFnsToScope: false
};
// Classy controllers will no longer automatically add functions to the `$scope`

or on a per-controller basis:

app.classy.controller({
    name: 'TodoCtrl',
    inject: ['$scope', 'filterFilter'],
    __options: {
        addFnsToScope: false
    }
    // ...
});

How do I ask a question that isn't answered here?

Open an issue on Github issues and I'll do my best :-)

todo-controller.coffee.js

Add ‘classy’ to your app modules

app = angular.module 'app', ['classy']

Register your controller and inject your dependencies. Injecting dependencies with Classy plays nice with minifiers, you don’t need to annotate your dependencies (i.e. list dependencies twice) and your code remains DRY.

By the way you can use the shortcut app.cC instead of app.classy.controller if you prefer.

app.classy.controller

  name: 'TodoCtrl'

  inject: ['$scope', '$location', 'todoStorage', 'filterFilter']

An init method for your initialization code. Who’d have thunk it? You can access your dependencies using the class-wide @ symbol. The $scope is available using @$ (or you can use @$scope if you prefer).

  init: ->
    @todos = @$.todos = @todoStorage.get() #!
    @$.newTodo = '' #!
    @$.location = @$location #!

Instead of polluting your init method with lots of calls to $scope.$watch, you can put your watchers in the watch object instead. If you want to watch an object or collection just use the {object} or {collection} keyword.

  watch:
    'location.path()': (path) ->
      @$.statusFilter = #!
        if (path is '/active') then completed: false #!
        else if (path is '/completed') then completed: true #!

    '{object}todos': '_onTodoChange'

Most of the time when you add a function to a controller, you want it available on the $scope. Classy automatically puts the function in your $scope so you can easily access it using directives like ng-click.

  addTodo: ->
    newTodo = @$.newTodo.trim() #!
    @todos.push #!
      title: newTodo #!
      completed: false #!

Prefix the function name with an underscore and Classy wont add

  _onTodoChange: (newValue, oldValue) ->
    @$.remainingCount = @filterFilter(@todos, completed: false).length #!

Add ‘classy’ to your app modules

var app = angular.module('app', ['classy']);

Registers your controller and inject your dependencies. Injecting dependencies with Classy plays nice with minifiers, you don’t need to annotate your dependencies (i.e. list dependencies twice) and your code remains DRY.

By the way you can use the shortcut app.cC instead of app.classy.controller if you prefer.

app.classy.controller({

  name: 'TodoCtrl',

  inject: ['$scope', '$location', 'todoStorage', 'filterFilter'],

An init method for your initialization code. Who’d have thunk it? You can access your dependencies using the class-wide this symbol. The $scope is available using this.$ (or you can use this.$scope if you prefer).

  init: function() {
    this.todos = this.$.todos = this.todoStorage.get(); //!
    this.$.newTodo = ''; //!
    this.$.location = this.$location; //!
  }, //!

Instead of polluting your init method with lots of calls to $scope.$watch, you can put your watchers in the watch object instead. If you want to watch an object or collection just use the {object} or {collection} keyword.

  watch: {
    'location.path()': function(path) {
      this.$.statusFilter = (path === '/active') ? //!
        { completed: false } : (path === '/completed') ? //!
        { completed: true }; //!
    }, //!

    '{object}todos': '_onTodoChange'
  }, //!

Most of the time when you add a function to a controller, you want it available on the $scope. Classy automatically puts the function in your $scope so you can easily access it using directives like ng-click.

  addTodo: function() {
    var newTodo = this.$.newTodo.trim(); //!
    this.todos.push({ //!
      title: newTodo, //!
      completed: false //!
    }); //!
  }, //!

Prefix the function name with an underscore and Classy wont add it to the $scope.

  _onTodoChange: function(newValue, oldValue) {
    this.$.remainingCount = //!
      this.filterFilter(this.todos, { completed: false }).length; //!
  } //!

}); //!

Installation

  1. Or install with bower: bower install angular-classy
  2. Reference Classy after the reference to Angular
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-classy/angular-classy.min.js"></script>
  3. Add Classy to your application module
    app = angular.module 'app', ['classy']
    var app = angular.module('app', ['classy']);
  4. That's it, you can create a classy.controller like so:
    app.classy.controller
      name: 'MyCtrl'
      inject: ['$scope']
      init: ->
    app.classy.controller({
      name: 'MyCtrl',
      inject: ['$scope'],
      init: function() { }
    });

More Info