Writing AngularJS controllers with CoffeeScript classes
When I started using AngularJS one of the obstacles I ran into was using CoffeeScript classes to develop the controllers. Most examples show an inline JavaScript function, which I can easily duplicate with CoffeeScript. However, to make use of a CoffeeScript class, I had to play around with it till I figured it out.
In this post I'll provide a look at converting the simple Todo app on the Angular page to CoffeeScript. I'll cover the process I went through while figuring this out, which includes:
- A 1:1 JavaScript to CoffeeScript conversion using functions
- Using a CoffeeScript class with all functions defined in the constructor, off of
$scope
(don't do this) - Defining methods on the class instead of on
$scope
and assigning the class to the$scope
(good) - Using the new Angular 1.1.5+
controller as
syntax and an example of CoffeeScript using a class and base class (good)
Original Todo App
To begin with, familiarize yourself with the original Angular Todo app written in JavaScript:
1:1 Conversion to CoffeeScript
When using CoffeeScript the output is typically generated within an anonymous function to avoid polluting the global namespace (unless you're using the bare
compilation option). This poses a challenge when converting the example from JavaScript to CoffeeScript. When doing so, you'll likely run into this error: Argument 'TodoCtrl' is not a function, got undefined
To address this issue:
- Add a module name for the Angular application:
<html ng-app="todoApp">
- Add the controller to the todoApp module in the CoffeeScript file:
angular.module("todoApp", [])
.controller("TodoCtrl", TodoCtrl)
The following JS Bin shows the 1:1 conversion result.
Using a CoffeeScript class with Methods on $scope
Great, we now have CoffeeScript code! To use a CoffeeScript class the first thing to figure out is how to use Angular dependency injection (DI). The answer is to pass everything as constructor parameters, as follows:
class TodoCtrl
constructor: ($scope) ->
$scope.todos = [
text: "learn angular"
done: true
,
text: "build an angular app"
done: false
]
Once you do that, you run into another error: Uncaught ReferenceError: $scope is not defined todo.js:23
It turns out all the methods being defined on $scope
cause that error since $scope
isn't defined. You could solve this by moving all the function definitions off of $scope
into the constructor.
This approach isn't recommended. It's not ideal and isn't making use of the CoffeeScript class. We'll fix that next.
Improved CoffeeScript Class Without Relying on $scope
To leverage a proper class we need to define the functions on the class instead of on the $scope
. Here are the changes I've made to the HTML and CoffeeScript files to facilitate this:
- Assign
$scope
to the class in the constructor. This is done by using the@
prefix:constructor: (@$scope) ->
- For now I've kept the
todos
array hanging off of $scope, which means we would need to refer to it via@$scope
in all methods. That's why step #1 was done. - Change all
$scope
methods to class methods
As soon as that's done we run into two issues:
Issue: archive functionality breaks. To fix it we need to use a fat arrow in the
angular.forEach
to maintain the proper scope or rewrite the loop. The former looks like this:archive: -> oldTodos = @todos @todos = [] angular.forEach(oldTodos, (todo) => @todos.push(todo) unless todo.done )
Issue: all methods bound to in HTML are hanging off
$scope
so they don't render to the page. The remaining count is missing and the text appears as "remaining of 2". TheaddTodo
method is broken too. We need a way to access the controller methods and this is done by assigning the controller to the$scope
in the constructor and updating the template to access the methods from the controller. Thus, we prefix all methods withctrl.
, e.g.,ctrl.remaining()
(same forarchive
andaddTodo
).constructor: (@$scope) -> # todos here $scope.ctrl = @
Here's the full JS Bin sample of this approach:
CoffeeScript Class, Base Class, and Controller As Syntax
The controller as syntax was introduced in Angular 1.1.5, and it allows us to achieve the same result as assigning the controller to the scope. Rather than doing so in the CoffeeScript file, we can move it to the markup which makes it much more readable.
In this final example I've made the following changes:
- Changed Angular library to 1.1.5+
- Moved
todos
array andtodoText
to the class and update their template references (no more$scope
reliance, except to log it to the console) - Used Controller as syntax and updated template to prefix
ctrl.
as needed - Introduced a
BaseCtrl
which theTodoCtrl
will inherit from to make use of thetoJson
base method - Added a
textarea
bound to the basetoJson
method - Applied DI via the
$inject
approach to address minification concerns
CoffeeScript and ng-min Incompatibility
Unfortunately if you used to depend on ng-min to convert the inline function DI approach to bracket notation it will no longer work with CoffeeScript classes. To address this you should use the $inject
property instead, which I demonstrated in the final example above.