coffeescript で AngularJS を書くと ng-click がうごかない


最近ようやく coffeescript に手を出しました。そこで AngularJS を書いてたら ng-click が見事に動かない!という現象にぶち当たりました。

答えは return により controller がコンストラクタとして働かなかったためでした。

抜けるための tips をかいておきます。

まずはサンプルを

javascript では、モダンプラクティス を参考にして、次のように書いてました。

javascript


myApp = angular.module('myApp', [])

//controllerを作成
var MainController = function() {
  var main = this;
  main.sayHi = function() {
    console.log('Hi!');
  };
};

//Directive Difinition Object を作成
var mainDDO = function() {
  return {
    restrict: 'E',
    controller: MainController,
    controllerAs: 'main',
    scope: {},
    template: '<input type="button" value="Hi" ng-click="main.sayHi()">' 
  };
};

myApp.directive('myappMain', mainDDO);

HTML

<myapp-main></myapp-main>

ボタンを押したら「Hi!」と返す、簡単でかわいいヤツ。

coffeescript で書いてみよう

これを coffeescript で書こうとして、まずはこうなりました。

coffeescript

MainController = ->
  main = @
  main.sayHi = ->
    console.log 'Hi!'

mainDDO = ->
  {
    restrict: 'E'
    controller: MainController
    controllerAs: 'main'
    scope: {}
    template: '<input type="button" value="Hi" ng-click="main.sayHi()">' 
  }

myApp.directive('myappMain', mainDDO);

main = @ のところははぶいて、いきなり@sayHi = -> でもいいのですが、controllerAs がイメージしやすいのでなんとなくこう書いてます。他のみなさんはどうなんでしょうか?

コンパイルするとこうなります。

javascript

(function() {
  var MainController;

  myApp = angular.module('myApp', []);

  MainController = function() {
    var main;
    main = this;
    return main.sayHi = function() {
      return console.log('Hi!');
    };
  };

  mainDDO = function() {
    return {
      restrict: 'E',
      controller: MainController,
      controllerAs: 'main',
      scope: {},
      template: '<input type="button" value="Hi" ng-click="main.sayHi()">' 
    };
  };

  myApp.directive('myappMain', mainDDO);

}).call(this);

しかし上のコードではうごきません。

なぜか?

http://thejsguy.com/javascript/angular.js/coffeescript/2014/12/29/angular-coffeescript.html
に詳しく書いてました。

答えは coffeescript によって補われる return 。

調べたところ、 Angular では controller はコンストラクタとして働くらしいです。確かにリファレンスにはっきりと書いてありました(汗

Angularでは、コントローラーはAngularのスコープを引数として使用するJavaScriptのコンストラクタ関数です。

ng-controllerディレクティブを介して、コントローラーがDOM要素に割り当てられると、 Angularは指定されたコントローラーのコンストラクタ関数を使用して、新しいコントローラーオブジェクトをインスタンス化します。 新しい子スコープは、$scopeとしてコントローラーのコンストラクタ関数へ注入されることで、引数として利用可能になります。

ここでは最終行が return されてしまったために、のぞんだ動きにならなかったのでした。

解決策

解決策は2つあります。

1. ちゃんと自分を返す

まずは return を意識して、最後に自分を返してあげるというもの。

coffeescript

MainController = ->
  main = @
  main.sayHi = ->
    console.log 'Hi!'
  main # @ でもOK

↓コンパイル

javascript

MainController = function() {
  var main;
  main = this;
  main.sayHi = function() {
    return console.log('Hi!');
  };
  return main;
};

2. coffeescript の class を使う

coffeescript なので class を利用できます。
この問題にぶち当たるまで知らなかったのはナイショだよ。

coffeescript

class MainController
  constructor: () ->
    main = @
    main.sayHi = ->
      console.log 'Hi!'

↓コンパイル

javascript

var MainController;

MainController = (function() {
  function MainController() {
    var main;
    main = this;
    main.sayHi = function() {
      return console.log('Hi!');
    };
  }

  return MainController;

})();

class を使うとすっきりしますね。

感想

coffeescript を初めて使ったことによる初歩的な罠でした。class を使ういい機会になりました。ようやくモダンプラクティスの内容がすっと入ってきた気がします。今後は TypeScript にも挑戦してみようと思います。