Class-based AngularJS Development 08 Jan 2014
  • Or “How to write documented, testable, well-structured 80col AngularJS code”

Growing pains

I’ve been using AngularJS for the majority of my client-side code for the past ~6 months. It’s amazing. Directives? Two-way binding? MVC on the client? Pinch me.

As the projects I’ve used it for have grown in size though, I’ve run into a few annoyances. In short, it’s really hard to re-use things “nicely”, it’s damn hard to document, and unit tests are ugly. I use codo to document all Adefy library classes, but I can’t use it quite the same way with inline methods. That, and I’m just used to testing class methods. Testing SomeClass.someContextuallyMeaningfulName is much more elegant than $scope.someMethod, or worse, randomInlineMethod.

Also, the inelegance of AJS controller/service/thing definitions in general really got to me. As an example:

1 window.App.controller "SomeController", ($scope, $routeParams, $http) ->
2 
3   do = "so"
4   much()
5   stuff -> without "any#{real}", structure

Something about that just irks me. It’s just begging for some proper structure. I skimmed through Code Complete recently, and fell in love with the “keep your methods small” schpeel.

So I went through my code, and split up large methods into many, many small and reusable ones. Then my controllers got really ugly. Methods just hanging in scope, not attached to anything, but just “existing”? Gah. Then I read some philosophy books, and it hit me.

Classes! Inheritance! Multi-level logging!

Imagine being able to write the above as:

 1 class AppSomeController
 2 
 3   depList: [
 4     "$scope"
 5     "$routeParams"
 6     "$http"
 7   ]
 8 
 9   do: null
10 
11   constructor: (so) ->
12     @do = so
13     @much()
14     @stuff -> "I can't think of anything to put here"
15 
16   much: -> #
17   stuff: (cb) -> cb() # Meaningless, but bear with me

The best part? You can document that nicely for codo like this:

 1 # Some really useful class definition
 2 class AppSomeController
 3 
 4   # @property [Array<String>] dependency list
 5   depList: [
 6     "$scope"
 7     "$routeParams"
 8     "$http"
 9   ]
10 
11   # Initializes us, and does some stuff
12   #
13   # @param [Object] somethingImportant a meaningless parameter
14   constructor: (so) ->
15     @do so
16     @_much()
17     @stuff -> "I can't think of anything to put here"
18 
19   # Does nothing useful! Doesn't even use the supplied argument
20   #
21   # @param [Object] arg meaningless argument
22   # @return [Number] randomNumber random number
23   do: (arg) -> Math.random()
24 
25   # @nodoc
26   _much: -> #
27 
28   # Calls the callback immediately. Speed with a passion
29   #
30   # @param [Method] callback
31   # @return [Object] result callback result
32   stuff: (cb) -> cb() # Meaningless, but bear with me

The beauty of this really shines through when you have some functionality you can attach to a base class. I prefer to disable log messages in production, so adding log methods to a base class, along with a modifiable log level, results in easy message filtering. Better yet, you can add functionality to log to a server when the log level is set to Production, or something similar.

Also, what about an easily configurable module name? So instead of having to edit every controller/service/directive/factory/etc, you could simply edit the module name on the base class.

Not to mention the structure. Proper controller/service/etc inheritance, easy testing, and all the flexibility that classes provide.

Yes yes yes!

Let’s dive in

Right, so, we need a base class. This will set our application name, and provide a minimal logging system. That is literally all the functionality present, so I’ll just go ahead and paste it in without too much explanation:

 1 # Serves as a useful base class for all other classes to inherit from. This
 2 # includes controllers, directives, services, etc. Allows us to specify the
 3 # module name in one place and whatnot :)
 4 #
 5 # Also provides a minimal logging system, to be used in all other classes.
 6 class AppBaseClass
 7 
 8   # @property [String] name of the angular module to attach all objects to
 9   @appName: "App"
10 
11   # @property [Number] log level, between 0 and 4, where 4 is most verbose
12   @logLevel: 4
13 
14   ##
15   ## Logging. I would have broken this out into a seperate class, but it's
16   ## useful to have methods on each object and whatnot. The logLevel is global,
17   ## being static on AppBaseClass.
18   ##
19   ## Log messages with levels between 1 and 4 respect filtering, whereas log
20   ## messages of level 0 are always visible
21   ##
22   ## Log levels:
23   ##   0 - Unlabeled and unfiltered
24   ##   1 - Error
25   ##   2 - Warning
26   ##   3 - Debug
27   ##   4 - Info
28 
29   @logLabels: [
30     "",
31     "[Error]> ",
32     "[Warning]> ",
33     "[Debug]> ",
34     "[Info]> "
35   ]
36 
37   # Main logging method. The level has to be between 1 and 4, and is capped
38   # otherwise (following a warning).
39   #
40   # @param [String] message
41   # @param [Number] level optional, between 1 and 4
42   log: (message, level) ->
43     param.required message
44     level = param.optional level, AppBaseClass.logLevel
45 
46     if level > 4
47       console.warn "Log level greater than 4, capping [#{level}]"
48       level = 4
49     else if level < 0
50       console.warn "Log level less than 0, capping [#{level}]"
51       level = 0
52 
53     labeledMessage = "#{AppBaseClass.logLabels[level]}#{message}"
54 
55     if level == 0 then console.log message
56     else if level == 1
57       if console.error then console.error labeledMessage
58       else console.log labeledMessage
59     else if level == 2
60       if console.warn then console.warn labeledMessage
61       else console.log labeledMessage
62     else if level == 3
63       if console.debug then console.debug labeledMessage
64       else console.log labeledMessage
65     else if level == 4
66       if console.info then console.info labeledMessage
67       else console.log labeledMessage
68 
69     message
70 
71   # Log an error directly. Alias for log message, 1
72   #
73   # @param [String] message
74   logError: (message) -> @log message, 1
75 
76   # Log a warning directly. Alias for log message, 2
77   #
78   # @param [String] message
79   logWarning: (message) -> @log message, 2
80 
81   # Log a debug message directly. Alias for log message, 3
82   #
83   # @param [String] message
84   logDebug: (message) -> @log message, 3
85 
86   # Log an info message directly. Alias for log message, 4
87   #
88   # @param [String] message
89   logInfo: (message) -> @log message, 4

The log level is a static value on the base class, allowing us to silence the entire application in production (silencing in this case means letting only errors through. One could easily add another truly silent log level).

The log methods are instance methods, meaning we can do @logDebug “Some message” in inheriting classes.

Now, we need base classes for directives, services, and modules. The tricky part here is that we need to create every directive/service/etc on our module, and we need to maintain a proper dependency and argument list so that injection doesn’t break.

Controller Classes

To implement this, we can pull the angular declaration itself into our constructor, and break out child-class constructors into a new method. Since dependencies are pseudo global within controller logic scope (injected at the top), I opted to bind dependencies as instance variables on controller classes. This means $scope turns into @$scope throughout controllers.

One other thing worth tweaking that really started to bug me, is the really long lines dependency lists tend to create. When you have six dependencies, four of which are services with very descriptive class names, word wrap tends to kick in (at least for me). I’m also a huge fan of sticking to a max width of 80 columns, so having 100 col+ method definitions becomes quite ugly.

We can’t easily get around this however, as Angular expects proper parameter names in method definitions, so it can inject dependencies.

In order to parse argument lists, Angular calls toString() on methods and reads the method definition. (“function (…) {“) So, we can override toString() on our controller’s initialization method to return a fake argument list, and then manually bind arguments onto our instance! :D

The code is quite a bit simpler than my explanation :) This is our controller base class:

 1 class AppController extends AppBaseClass
 2 
 3   depList: ["$scope"]
 4 
 5   # Initializes our AJS controller with our init method
 6   #
 7   # @param [String] name controller name
 8   constructor: (@__initName) ->
 9 
10     # Setup dependencies by overriding toString()
11     @_preInit._toString = @_preInit.toString
12     @_preInit.toString = =>
13       rawLines = @_toString().split "\n"
14       rawLines[0] = "function ("
15 
16       # Build a fake argument list, from our dependencies
17       for dep, i in @depList
18         rawLines[0] += "@#{dep}"
19         rawLines[0] += "," if i != @depList.length - 1
20 
21       rawLines[0] += ") {"
22 
23       rawLines.join "\n"
24 
25     # Add our _preInit method to the dependency list, and ship it
26     # to Angular!
27     @depList.push @_preInit
28     window[AppBaseClass.appName].controller @__initName, @depList
29 
30   # Binds injected dependencies, as per our depList. Aka, "$scope"
31   # becomes "@$scope"
32   _preInit: =>
33 
34     # Bind injected dependencies
35     for i in [0...arguments.length]
36       if @depList[i] != undefined
37         @[@depList[i]] = arguments[i]
38 
39     # Call real init
40     @init()
41 
42   # Actual init method, which should be overriden in inheriting
43   # classes
44   init: -> @logWarning "Undefined initialization for #{@__initName}"

Note how we build the new string representation by modifying the method definition in lines 14-23, and then bind injected dependencies ($scope, $http, etc) with _preInit.

Also, since _preInit gets called from within Angular itself, we need to define it with this bound to the owning instance of AppController. Hence the fat arrow. (=>)

Now, what does an actual controller in our application look like with this architecture? Simplistic, documented, and testable:

 1 # Some really awesome documentation for codo
 2 class SomePageController extends AppController
 3 
 4   # Builds us as an AJS module
 5   constructor: -> super "SomePageController"
 6 
 7   # @param Dependency list!
 8   depList: [
 9     "$scope"
10     "$location"
11     "userService"
12   ]
13 
14   # @nodoc
15   init: ->
16     @initializeScope()
17     @initializeUI()
18     @bindListeners()
19     @fetchData()
20 
21   # Document these properly...
22   initializeScope: -> # ...
23   initializeUI: -> # ...
24   bindListeners: -> # ...
25   fetchData: -> # ...
26 
27 # Create an instance
28 new SomePageController()

Keep in mind that only one instance of a controller like the one above should ever exist, since it creates itself with a static name (“SomePageController”). If you want to re-use a single controller for multiple pages with different parameters, then pass the controller name into the constructor. Better yet, break out true initialization into a seperate method, so the controller can be configured after instantiation.

Or, keep it as above, and just roll with the class structure for documentation/testing/neatness and inheritance :)

Premature conclusion

I was initially going to give base classes for servers and directives, but they are so similar to the controller base class that any examples would be redundant.

Essentially, the dependency handling code above does all the heavy lifting. Now you can document all of your projects’ classes with something like codo, and bask in the glory of auto-generated HTML documentation. That, and tests. Just create an instance of your controller for the test, and provide mock data (better yet, mock a service, then instantiate the controller).

AngularJS code just got beautiful. Enjoy!