1.0 released (2nd March 2015).
Now supports plugins and other new features.

Add structure to your AngularJS controllers

Angular's controllers are super-simple which is fantastic for getting started (they're just functions). Because they are just functions, there's lots of different ways to write Angular controllers. This is powerful but can also lead to a lack of structure, especially when many people (with many different coding styles) are working on the same codebase.

It can be helpful to have something a little more structured and prescriptive. Enter Angular Classy!

The code to the right is a Classy controller for a simple Todo application. Take a look, Classy keeps your controllers clean and manageable.
You can play with a live example on plunker.

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


Injecting Dependencies

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

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

Accessing $scope and dependencies

Dependencies are available using @DependencyNamethis.DependencyName.

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 @$scopethis.$scope if you prefer.

Want to use controllerAs instead of $scope? ▾

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 data/methods from being added to the $scope by simply changing the config. You can do this on a per-module basis:

app.classy.options.controller = {
  addToScope: false
};

or on a per-controller basis:

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

Initialisation

Classy provides a canonical place to init things. It's an init method!

At a glance you can quickly see what your controller does when it initialises, no more searching through your controller. Simples!

Data Initialisation

There is also a data property available for defining initial data properties. This will enable you to remove a lot of boilerplate assignment out of your init method.

The data object allows you to use the full power of Angular expressions when defining data properties.

data: {
  todos: 'todoStorage.get()',
  editedTodo: null
}
data:
  todos: 'todoStorage.get()'
  editedTodo: null
What's the alternative syntax? ▾

If you prefer you can use a function that returns an object, this allows you to reference other class properties directly (rather than through an angular expression).

data: function() {
  return {
    todos: this.todoStorage.get(),
    editedTodo: null
  }
}
data: ->
  todos: this.todoStorage.get()
  editedTodo: null

Watching Properties

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 Equivalent $watch Type
{collection} or {shallow} $watchCollection(..)
{object} or {deep} $watch(.., objectEquality = true)

Methods

Most of the time when you add a method 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 is how methods look with Classy:

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

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

Method Expressions

You can now define methods using angular expressions. Whenever the method is called it will evaluate the expression and return the expression's result. Often, an expression will be much more concise and readable than a full method definition.

methods:
  getLast5CompletedTodos: 'todos | filter:{completed: true} | orderBy:"timestamp" | limitTo:5'
methods: {
  getLast5CompletedTodos: 'todos | filter:{completed: true} | orderBy:"timestamp" | limitTo:5'
}

Plugins

Angular Classy now supports plugins so you can extend Classy with useful features that allow you to write computed properties and even extend controller classes.

If you want to write your own plugins then head on over to the classy-plugins repo for more information. This will be expanded with more detail over time, if you have a question then raise an issue.

Here is a list of the current Classy plugins (dynamically queried from Bower):

Name Author Stars

Only 2KB (gzipped and minified)

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


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 data/methods from being added to the $scope by simply changing the config. You can do this on a per-module basis:

app.classy.options.controller = {
  addToScope: false
};

or on a per-controller basis:

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

Can I chain Classy Controllers? ▾

Earlier versions of Angular Classy didn't allow chaining (of services, routes and factories etc.) with Classy controllers. This was done to allow you to use Classy controllers inline with directives.

We are maintaining support for directives but also introducing a new syntax: classy.controllers (notice the 's' at the end) that takes an array of controllers and supports chaining so now you can do:

angular.module('app', ['classy'])
  .classy.controllers([{
    name: 'BarController',
    inject: ['$scope'],
    init: function() {
      this.$.foo = 'bar';
    }
  }, {
    name: 'BazController',
    inject: ['$scope'],
    init: function() {
      this.$.foo = 'baz';
    }
  }])
  .service(
    // Service here
  )
  .config(
    // Config here
  );
angular.module("app", [ "classy" ])
  .classy.controllers([
    name: "BarController"
    inject: [ "$scope" ]
    init: -> @$.foo = "bar"
  ,
    name: "BazController"
    inject: [ "$scope" ]
    init: -> @$.foo = "baz"
  ]).service(
    # Service here
  ).config(
    # Config here
  )

What's performance like? ▾

I've done some profiling using the timeline in Chrome Developer Tools and performance seems to be in the same ballpark as a vanilla AngularJS controller.

I've also done some synthethic benchmarking, and it turns out that method call performance is as good as vanilla Angular controllers.

By default, Classy follows best practices like putting methods on the prototype (not all Angular programmers do this) so you might even find that Classy performs better in some instances.

But more performance testing is always a good thing so I hope to do more benchmarking in the future.

What's on the roadmap for future versions? ▾

Next up I want to take a look at Classy services and directives.

Sorry, but I don't like the look of Classy controllers! ▾

No need to apologise. I created the library for myself first and foremost, personally I find it helps me write really strucutred and readable controllers. It's definitely opinionated and it won't suit everybody but I'm ok with that. :)

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: 'TodoController'

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

New in 1.0! The data object is a simple structure that allows you to easily assign properties.

You can either use a string representing an angular expression, or you can directly assign any other object/primitive.

Classy automatically makes items in the data object available class-wide (and on the $scope).

This feature helps to move boilerplate assignments out of the init method.

  data:
    todos: 'todoStorage.get()',

An init method for your initialization code! You can access your dependencies using the class-wide @ symbol. The $scope is available using @$ (or you can use @$scope if you prefer).

  init: -> @_resetEntryField()

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:
    '{object}todos': '_onTodoChange'

New in 1.0! Controller methods are defined inside of the methods object.

Classy automatically makes methods available class-wide (through @methodName) and also on the $scope so you can easily access it using directives like ng-click.

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

You can also define methods using angular expressions (see _getRemainingCount()). Whenever the expression method is called it will evaluate the expression and return the expression’s result.

  methods:

    _getRemaining: 'items | filter:{ complete: false }'

    _onTodoChange: ->
      @$.remainingCount = @_getRemaining().length #!
      @todoStorage.put @items #!

    addTodo: ->
      @$.items.push #!
        text: @$.newTodo #!
        complete: false #!
      @_resetEntryField() #!

    _resetTodoEntry: -> @$.newTodo = ''

Add ‘classy’ to your app modules

var 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: 'TodoController',

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

New in 1.0! The data object is a simple structure that allows you to easily assign properties.

You can use a string representing an angular expression, or you can directly assign any other object/primitive.

Classy automatically makes items in the data object available class-wide (and on the $scope).

This feature helps to move boilerplate assignments out of the init method.

  data: {
    items: 'todoStorage.get()'
  },

An init method for your initialization code! 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._resetEntryField();
  },

Instead of polluting your init method with 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: {
    '{object}items': '_onTodoChange'
  },

New in 1.0! Controller methods are defined inside of the methods object.

Classy automatically makes methods available class-wide (through this.methodName) and also on the $scope so you can easily access it using directives like ng-click.

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

You can also define methods using angular expressions (see _getRemainingCount()). Whenever the expression method is called it will evaluate the expression and return the expression’s result.

  methods: {

    _getRemaining: 'items | filter:{ complete: false }',

    _onTodoChange: function() {
      this.$.remainingCount = this._getRemaining().length; //!
      this.todoStorage.put(this.items); //!
    },

    addTodo: function () {
      this.$.items.push({text: this.$.newTodo, complete: false}); //!
      this._resetEntryField(); //!
    },

    _resetTodoEntry: function() {
      this.$.newTodo = ''; //!
    }

  }

}); //!

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