Use a "Coffee Script Class" instead of a Method as Angular JS ng-controller

I want to do something that I think will be a good way to use "Coffee Script Class" and Angular JS structures.

<!doctype html>
<html ng-app>

  <head>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width, initial-scale=1">
    <title>Main Test</title>
    <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">
    <script type="text/javascript" src="js/jquery-1.8.3.min.js"></script>
    <script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="js/angular.min.js"></script>
    <script type="text/javascript" src="js/coffee.js"></script>
  </head>

  <body>
    <div ng-controller="MainClass" style="margin-left:10px">
      <h2>All todos: {{test()}}</h2>
    </div>
  </body>

</html>

Notice than I setup the DIV ng-controller as MainClass and binding test() method inside a H2 HTML tag.

class AngularJSController
  constructor: ($scope, $main) ->

    $scope.test = MainClass.prototype.test
    MainClass.test = MainClass.prototype.test
    $main.test = MainClass.prototype.test
    test = MainClass.prototype.test
    @test = MainClass.prototype.test

    console.log @test

class MainClass extends AngularJSController
  constructor: ($scope) ->
    super $scope, this

    setTimeout (->
        console.log test()
      ), 1000

    test();

  test: -> 'aloha!'

In AngularJSController constructor I've tried all forms I imagined to setup my super class method TEST on MainClass scope without success.

I'm trying to do it because I want to work with classes just on my Angular JS controllers and components.

Problems I already fell:

  1. If I try to use @test() instead of test() inside setTimeout, jQuery already replaced the this property with a kind of JQuery Window Object.

    setTimeout (-> console.log @test()), 1000
    
  2. I don't know what really the scope of test() calls, if a place this (or @ cause Coffee), isn't the same of place anything.

    test() != this.test() != @.test() # the first scope isn't the same scope of last two calls
    

I have used the following syntax:

app = angular.module 'myapp', []

class MySimpleCtrl

  @$inject: ['$scope'] 
  constructor: (@scope) ->
    @scope.demo = 'demo value'
    @scope.clearText = @clearText

  clearText: =>
    @scope.demo = ""

app.controller 'MySimpleCtrl', MySimpleCtrl

angular.bootstrap document, ['myapp']

Take a look at this jsFiddle: http://jsfiddle.net/jwcMA/

Here's a generic approach with a base class:

http://www.devign.me/angular-dot-js-coffeescript-controller-base-class/

BaseCtrl.coffee

# dependency - Function.prototype.bind or underscore/lodash

app = angular.module 'someApp'

class @BaseCtrl
  @register: (app, name) ->
    name ?= @name || @toString().match(/function\s*(.*?)\(/)?[1]
    app.controller name, @

  @inject: (args...) ->
    @$inject = args

  constructor: (args...) ->
    for key, index in @constructor.$inject
      @[key] = args[index]

    for key, fn of @constructor.prototype
      continue unless typeof fn is 'function'
      continue if key in ['constructor', 'initialize'] or key[0] is '_'
      @$scope[key] = fn.bind?(@) || _.bind(fn, @)

    @initialize?()

BookFormCtrl.coffee

app = angular.module 'someApp'

class BookFormCtrl extends BaseCtrl
  @register app
  # list of dependencies to be injected
  # each will be glued to the instance of the controller as a property
  # e.g. @$scope, @Book
  @inject '$scope', 'Book'

  # initialize the controller
  initialize: ->
    @$scope.book =
      title: "Hello"

  # automatically glued to the scope, with the controller instance as the context/this
  # so usable as <form ng-submit="submit()">
  # methods that start with an underscore are considered as private and won't be glued
  submit: ->
    @Book.post(@$scope.book).then =>
      @$scope.book.title = ""

  1. setTimeout isn't related in any way to jQuery, but function passed to setTimeout is indeed executed in the global(window) context. Use fat arrow to explicitly bind it to the current scope. http://coffeescript.org/#fat_arrow

    setTimeout (=>
      console.log @test()
    ), 1000
    
  2. Sorry, I'm not sure what are you asking about.

Angular does supports Coffescript classes!, for the most part. I've found issues with using RequireJs, expecting a function instead of an object.

take a look here : http://softwareninjaneer.com/blog/writing-angularjs-controllers-coffeescript-classes/

I didn't have luck with @malix's answer although it did lead me to a solution that works. Here's how I'm doing mine:

'use strict'

class ImportsCtrl
  constructor: ($scope, Import) ->
    Import.query().then (imports) -> $scope.imports = imports

angular.module("greatDealsApp").controller "ImportsCtrl", ImportsCtrl

And here's a contrived example of turning $scope into a property of ImportsCtrl:

'use strict'

class ImportsCtrl
  constructor: ($scope, Import) ->
    @scope = $scope
    @Import = Import
    @loadImports()

  loadImports: =>
    @Import.query().then (imports) => @scope.imports = imports

angular.module("greatDealsApp").controller "ImportsCtrl", ImportsCtrl

And I don't know for sure if this matters, but I'm using ng-annotate.

It's impossible. You can't set a controller as an object (MainClass), because AngularJS ensures that the controller is a function.

You need to customize AngularJS to do it. I think it's not a good idea.

References