AngularJS's tutorial あなたとともにAngularJS

AngularJSの入門用のチュートリアルを作ってみました

なかなかまだ日本語の記事がなかったので、少しだけですが使ってみたことをメモしておきます

TwitterBootstrap、google-code-prettify、CoffeeScript、Sass、Compass、Grunt.js

by hisasann

Github

テキストボックスに ng-model を付与し、反映したい変数を入れる

<input type="text" ng-model="name">

あとは好きな場所に出力のコードを書くだけ!

{ {'あなたの名前は ' + name} }

サンプル

名前:

名前: 'あなたの名前は' {{name}} です

ng-controller="controller1"

でコントローラーを使う範囲をdivタグなどで囲います

囲われたdivタグの中ではコントローラーのほうでセットされた変数を見ることができまる

たとえばusersという配列をプロパティにセットしている場合は

$scope.users = [{name: "hisasann", score: 10}];
{ {users.length} }

{ {user.name} } { {user.score} }

などが使えます

また配列をループしたい場合は、

ng-repeat="user in users"

のように ng-repeat 属性を付与し、eachすることが可能です

サンプル

length: {{users.length}}

パイプでnumberやcurrencyを渡す

フィルターは非常に便利で、主に数値や日付、または文字列のそうさなどいろいろなことが可能です

{ {20 * 500|number} }

で3桁ごとにカンマを入れられたり、

{ {today|date:"yyyy-MM-dd"} }

で日付のフォーマットを変えられます

カンマを入れる {{20 * 500|number}}
下何桁 {{20 * 500|number:3}}
ドル {{30 * 500|currency}}
{{30 * 500|currency:"¥"}}
日付 {{today|date:"yyyy-MM-dd"}}

ループ時にリミットを決めたり、orderByを指定したり、便利なフィルターがあります

このあたりが非常に面白いところですね!

ng-repeat="user in users|limitTo:2|orderBy:'-score'"

orderByにはuserモデルの中にある score を指定しています

はじめにマイナスが付いているの降順の指定です

サンプル

複数あるリストを入力値によって絞り込みをしたい場合に活用できます

ng-model="query.name"

テキストボックスにの query を入れて、クエリ可能なテキストボックスにします

このとき、 query.name のようにドットでモデルのプロパティをしていしておくと、
そのプロパティでのみ絞り込むことが可能です
プロパティの指定がないとすべてのプロパティが対象になります

サンプル

ng-repeat="user in users"

を指定したループするタグに

ng-class-even="'even'" ng-class-odd="'odd'"

のように、 ng-class-evenng-class-odd にクラスを指定するだけです
一点気をつけないといけないのは、クラスを指定するところにはダブルコートの中にさらにシングルコートを入れているところです

index : {{$index + 1}} first : {{$first}} middle : {{$middle}} last : {{$last}} {{user.name}} - {{user.score|number}}

これで簡単に偶数行奇数行で色を変えることができます

外側のdivタグに ng-controller="controller1" が付いていて、
ネストした内側に ng-controller="controller2" がある状態にします
そしてネストされた中で ng-click="show()" のようにclickイベントをbindします

modalBody = $('#modalBody')
modal = $('#modal')

window.controller2 = ($scope) ->
  $scope.show = () ->
    modalBody.html($scope.user.name)
    modal.modal({backdrop:true})

ネストしたcontroller2のほうで、showイベントを追加しています
ここでのポイントは $scope.user.name のようにeachしているところの変数にアクセスできる点です

この値をModalに渡して表示しています

index : {{$index + 1}} name : {{user.name}} score : {{user.score|number}}

submit時のイベントは ng-submit にコントローラーのメソッドを指定します

ng-submit="appendUser()"

バリデーションには required ng-minlength="5" ng-maxlength="10" のようにただ指定するだけです

種類に関しては詳しくはこちらをご覧ください AngularJS: input

バリデーションでエラーが発生した場合に、エラーの要素を表示する必要があります
これには

ng-show="form1.name.$error.required"

のようにformのnameから始まりチェックしたい要素のnameを指定します

これだけでエラーの要素が表示されたり非表示になってくれます。簡単!

チェックボックスは

ng-true-value="YES" ng-false-value="NO"

のようにチェックが入った場合とチェックが外れた場合の値をセットできます

セレクトボックスは本来selectタグをoptionタグを書く必要がありますが、AngularJSは優秀で、

ng-options="kind for kind in ['ラーメン屋', 'つけ麺屋', '家系']"

のように、モデルの中身を指定するとoptionタグを自動で生成してくれます
↑のサンプルだとピンとこないですが、実際には以下のようになります

ng-options="user.name for user in users"

ちなみにformタグに novalidate 属性を付与していますが、これはHTML5のバリデーションが働かないようにしています

お店の名前
お店の名前が入力されていません!
少ないよ、もっと入れてよ!
そんなに入れちゃいやだよ!
あなたのメアド
どうして捨てアドなの?ねぇ、ねってば!
ボッタクリ店?
メモ {{20 - shop.memo.length}}
多すぎ!笑
何屋さん?

append時はshop.nameに値がある場合のみ追加するようにしています
この値がある状態というはバリデーションを突破した場合にのみ入るので、JavaScriptで自前でバリデーションする必要がありません

$scope.appendUser = () ->
  if this.shop.name
    this.shops.push this.shop

modelの変化を観察するのに、以下のようにjson出力しています
こうゆうのを手軽に用意しているあたりがにくい!

{ {shop|json} }
shop : {{shop|json}}
shops : {{shops|json}}

参考リンク:AngularJS: json

jQueryのようにわりと簡単にかける
$httpへのcacheオプションに渡せるのは boolean$cacheFactory
falseを入れるとキャッシュが有効にならないので、毎回リクエストされるが、$cacheFactoryを渡すとキャッシュが有効になる
ここの使い方は今後調査する

AngularJS: $cacheFactory

$http(
  method: $scope.method
  url: $scope.url
  cache: $templateCache
  ).success((data, status) ->
    $scope.status = status
    $scope.data = data

    $scope.name = data.name
    $scope.siteUrl = data.url
  ).error (data, status) ->
    $scope.data = data || "Request failed"
    $scope.status = status

相変わらず$scopeの値を書き換えるとすぐにViewに反映されるのは便利だ

http status code: {{status}}
http response data: {{data}}
name : {{name}}
url : {{siteUrl}}

event.targetScopeで取ってみる

$onでイベントをセットしておいて、 $emitでイベントをfireする
WebSocketっぽさがありますね 値の取得は、ハンドラ内で event.targetScopeを指定すると$emitしたcontrollerの$scopeが取れるので、 あとはそこから値を取るだけになります

window.controller1 = () ->
  # actionイベントハンドラ
  $scope.$on 'action', (event) ->
    $scope.showUser = event.targetScope.showUser

window.controller2 = () ->
  $scope.show = () ->
    $scope.showUser = this.user.name

    # emitでイベントをfire
    $scope.$emit 'action'

$emit以外に $broadcastというのがあるが、
これは以下のリンクがわかりやすい
$emitは自分自身と親のcontroller内のイベントをfireするが、$broadcastは自分自身と子どもたち内のイベントをfireする

AngularJS scope Example - jsFiddle

$emitでイベントをfireするときにハッシュを渡す

こっちのほうが読みやすいですね
↑の方法だとcontroller2に不必要に$scope.showUserというプロパティを作らないといけなかったんですが、
こちらの方法だと引数として渡せるので直感的です

window.controller1 = () ->
  # actionイベントハンドラ
  $scope.$on 'action', (event, args) ->
    $scope.showUser = args.showUser

window.controller2 = () ->
  $scope.show = () ->
    # emitでイベントをfire
    $scope.$emit 'action', showUser: this.user.name

AngularJSを使い始めた頃、$scopeってPHPみたいだからscopeにしようと思って実行してみたらこれがまた動かない
なので、いったいこの仮引数を固定させるのをどうやってるのかなーと追ってみたら、ただ単純にfunction文字列から正規表現で引数を抜き取って
チェックしているだけだった

がぴょーーーん!

一応それをやっている箇所をメモっておく

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function annotate(fn) {
  var $inject,
      fnText,
      argDecl,
      last;

  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      $inject = [];
      fnText = fn.toString().replace(STRIP_COMMENTS, '');
      argDecl = fnText.match(FN_ARGS);
      forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
        arg.replace(FN_ARG, function(all, underscore, name){
          $inject.push(name);
        });
      });
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn')
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;
}

$injectを使って依存性の注入ができるようだ
ここでの注入とは、コントローラーが使うべきserviceを固定することとぼくは捉えている
というのもコントローラーが使うべきservieは$httpのように引数に書けばそれが使えるが、$injectに渡された配列によって、
これ以外のserviceを使えなくしている

serviceを作るにはfactoryメソッドを使う

{{message}}

angular.module('controller4Module', []).factory('controller4Service', () ->
  message: 'controller4Message'
)

window.controller4 = ($scope, controller4Service) ->
$scope.message = controller4Service.message;

window.controller4.$inject = ['$scope', 'controller4Service']

実際、以下の行はなくても動く

window.controller4.$inject = ['$scope', 'controller4Service']

さらに言うと、以下の$httpはundefinedになる
これは$injectによって依存性が注入されていないからだと思われる

window.controller4 = ($scope, controller4Service, $http) ->

AngularJS: Injecting Services Into Controllers

AngularJS: $injector

directiveとはdirectiveが指定さえたタグの中を操作する、AngularJSの内部では極々一般的に使われているもののようだ
たとえばngRepeatやngControllerなどngなんちゃらと付くものはだいたいdirectiveでできている
さらにはangular-ui/modules/directives/animate/animate.jsのようなAngularUIたちももちろんdirectiveでできている

なので自分オリジナルなdirectiveを作る場合に、効果が発揮される
ただし、わりと理解しにくる箇所が多々あるので、ぼくも勉強中であります!

directiveもfactory同様moduleに追加していく

window.controller5 = ($scope, $http, $templateCache) ->

angular.module('controller5Module', [])
  .directive('controller5Directive', () ->
    (scope, element, attrs) ->
      scope.hello = 'controller5'
  )

上記のようにcontroller5Directiveというdirective名にした場合、HTMLのほうでは

<div controller5-directive>{{hello}}</div>

のように大文字になるところをハイフンで繋ぐようです

サンプル

{{hello}}

では今度はもう少しだけ進んで、ボタンをクリックされたらフェードアウトするという機能をdirectiveを使ってやっていみましょう
directiveの作り方は上記と同じですが、関数の中身を変えていきます

window.controller6 = ($scope) ->

angular.module('controller6Module', [])
  .directive('controller6Directive', () ->
    (scope, element, attrs) ->
      scope.hello = 'controller6'

      jqElem = angular.element(element)
      jqElem.find('#fadeOut').bind('click', () ->
        $(jqElem).fadeOut attrs.duration
      )
  )
<div controller6-directive duration="2000">
{ {hello} }
<button class="btn btn-primary" id="fadeOut">fadeOut</button>
</div>

directiveに渡した関数には3つの引数がありますが、elementとattrsを使ってみます
elementにはdirectiveが指定された要素が入っています
attrsにはdirectiveが指定された要素の属性が入っています

ここではフェードアウトするdurationを属性に指定して、その値をもとにフェードアウトのスピードをコントロールしています

気になるのは引数のelementはネイティブの要素なのかそれともAngularJSが装飾しているのか、はたまたjQueryオブジェクトなのかという点ですが、
実際にはAngularJSがそれっぽく装飾したオブジェクトが入ってきます(jqLite)
AngularJS: elementにメソッド一覧がありますが、結構貧弱です
とくにfindメソッドが「Limited to lookups by tag name.」となっているのでid指定ができません
そもそも要素の操作自体はAngularJSに任せるのではなく、jQueryを使いたいところです
AngularJSはDOMContentLoadedが呼ばれる前にjQueryが読み込まれていたら、そっちが使えます
ただし、そのままではjqLiteのままなので、

jqElem = angular.element(element)

でjQueryオブジェクトを取得します
これで馴染みの操作が可能になります

$(jqElem).fadeOut attrs.duration

あとはattrsからdurationを取得し、jQueryのfadeOutメソッドに渡しています

サンプル

{{hello}}

AngularJS and DOM Manipulation - YouTube

AngularJS: Directivesには2種類のdirectiveの使い方が書かれている
1つは上記で説明したdirectiveの第2引数でfunctionを返す方法
2つ目はfunctionを返さずにハッシュを返す方法
2つ目の方法は、directiveで指定したタグ内を自由に書き換えることができる、つまりいろいろできる版だ

問題は、かなりややこしいという点
directiveを指定するとそれっぽくその中身を書き換えることを考えるとテンプレートが必要ですが、そういった機能をそこそこきれいに書けたりする

まずはHTML部分を見ていただきたい

コントローラーの中にng-modelと、controller7-directiveが指定されたタグがある
キモとなるのはdirectiveが指定されたタグのほうだ
タグの中には何も指定されていないがサンプルで見れるように中にいろいろと要素がある
重要なのはdirectiveが指定されたタグにある属性たち、これがdirectiveに渡せる情報になる

<div ng-controller="controller7">
  <p>以下のテキストボックスはdirectiveの親のスコープにある</p>
  <p><input type="text" ng-model="note"></p>
  <div controller7-directive name="hisasann" note="note" alert-message="showMessage(kindKey)">
  </div>
</div>
do () ->
  window.controller7 = ($scope) ->
    $scope.note = 'default note'

    $scope.showMessage = (kind) ->
      window.alert kind

  angular.module('controller7Module', [])
    .directive('controller7Directive', () ->
      template: '<p>Hello World! {{name}} {{note}}</p>' +
                '<p>以下を入力してからalertを押してください</p>' +
                '<p><input type="text" ng-model="kind"></p>' +
                '<p><button class="btn btn-primary" ng-click="alertMessage({kindKey: kind})">alert</button></p>'   # or templateUrl: ""
      replace: false
      restrict: "A"
      scope: # use "@", "=", or "&"
        name: '@'   # bind attribute
        note: '='   # parent scope
        alertMessage: '&'   # Expression
      link: (scope, element, attr) ->
        scope.$watch(
          'kind'
          (newValue, oldValue) ->
            console.log newValue, ' ', oldValue
        )

        setTimeout(->
            scope.name = 'rastaman'
            scope.$digest()   # fire all the watches
          , 2000)
    )

templeteは外部に抜き出してtempleteUrlとしてもよいみたい

scopeのnameはdirectiveが指定されたタグに書かれている属性で、noteはdirectiveが指定された要素の親のスコープにあるmodel、
alertMessageにはメソッドの実行コードが入っている

linkというメソッドとは別にcompileというメソッドもあるのだが、compileのほうはscopeが引数に取れないので今いち使いドコロがわかっていない
さらにlinkとcompileは両方書いてもどちらか一方しか動かない(これもハマった…)
なのでlinkを使っている

ちなみにtemplateに書いたものはいつの間にかdirectiveが指定されたタグの中に展開されるので、ここが面白いところ
linkの中では$watchで値の監視をし、$digestで値の変更を反映している、非同期にmodelの値を書き換えた場合はどうも$digestしないと反映されないみたいだ

上記コードで一番やっかいだったのが、scopeに書いてあるalertMessageが動く仕組みだ br alertMessageには&が指定されているので、何かしら評価される式が入っている
HTMLのほう見てみると中身にshowMessage(kindKey)と書かれている、つまりalertMessageを呼ぶと間接的にshowMessage(kindKey)が呼ばれる
もちろんこのshowMessage(kindKey)はdirectiveが存在するscopeに無ければ動かない
さらにそのalertMessageを実行するボタンはdirectiveのtemplateに存在している、

ng-click="alertMessage({kindKey: kind})"

この部分だが、これはkindというmodelをkindKeyというkeyとしてshowMessageに渡している
どうもハッシュとして渡さないとkindをうまく渡せなかったので、これが定石なパターンなのか不明だが、記事をいろいろ見るとこうしているケースが多々あるので今はこれに落ち着いた

$scope.showMessage = (kind) ->
window.alert kind

これだけの過程を経て、やっとこのalertが表示される(ながっ!)

directiveの使い方としては、AngularJS Sticky Notes - jsFiddleAngularJS Isolated Scope Experiment - jsFiddleなんかが面白い

AngularJS Sticky Notes Pt 2 – Isolated Scope | One Hungry Mindここを読むとヒントが得られるかもしれない

restrict(制限)の種類

制限という名前になっているので分かりにくいが、要はdirectiveをどう指定したか
要素として書かれたのか属性として指定されたのかということ
割りと要素としてそのまま書いている記事が多いのでそれが主流っぽさがある

E Element name
A Attribute
C Class
M Comment

scopeの種類

どうもいつのバージョンかでこの書き方が変わったらしい
ネットの記事では@の部分をbindとしているのを見かけるが今はbindとしてももちろん動かない(これでかなりハマった…)

割りと使うのは@かなーと思うが、これはcompileメソッドやlinkメソッド内のattrs引数から取得できるので取れなくもない
ただしテンプレートとに何もせずともbindされるので使えるのかな

@ 属性の値をテンプレートにbindする
= 親のスコープにあるmodelを使う
& 親スコープのコンテキストで式を実行する

サンプル

以下のテキストボックスはdirectiveの親のスコープにある

[AngularJS]directiveを作ってみる。 | fine:der.

AngularJS - Directive Tutorial - YouTube

AngularJS - Custom Components - Part 1 - YouTube