MVWフロントエンド・フレームワーク、AngularJS をはじめてみる

AngularJS は、クライアントサイド MVC フレームワークの1つですが、自身のことを MVW フレームワークと呼んでます。 これは Model-View-Whatever の略で 「MV*について議論するなんて時間の無駄、そんな暇あるならコード書きなよ。MV*の*の部分なんて”Whatever”でいいんだよ。」 という思いが込められてるそうです・・・いかしてますね!
という訳でコードを書きながら AngularJS を紹介したいと思います ^^;

はじめに

2000 年頃から「web アプリでいかにネイティブアプリに迫れるか」をテーマに半分趣味で JavaScript と向かい合ってきた身としましては、 就活せまられる現状に乗じ、あわよくばフロントエンドエンジニアに転身できないかなぁ・・・などと思いを馳せつつ転職サイトを検索したりしております。

そんな中、フロントエンドで採用されてる技術としてよく目につくのが AngularJS です。 最近は賛否両論な意見をよく目にする AngularJS ですが、想像以上に世間では使われてるんだなぁ という印象をうけてます。

人気上昇中のJavaScriptライブラリを調べてみた【2015年版】 という記事でも MVC系フレームワーク部門で利用率はだんとつの1位のようです。

mvc

そんなわけで AngularJS 体験のファーストステップをまとめてみたいと思います。必要となるライブラリは angular.js 本体のみです。

https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js

今回も Backbone.js の時と同様(「Backbone.js ではじめるクライアントサイド MVC プログラミング – CYOKODOG」)、最初に TODO アプリを作っていってみたいと思います。

モジュールの定義

AngularJS でアプリケーションを作成する際、最初にモジュールの定義を行います。 今回作成するのは TODO アプリなので taskApp という名前でモジュールを定義してみます。

var app = angular.module('taskApp', []);


このモジュールを適用する場所、つまり HTML 上の TODO アプリを出力する要素に ng-app 属性を記述し、モジュール名である taskApp を指定します。

<div ng-app="taskApp">
    <!-- ここに TODO アプリを出力する -->
</div>

コントローラの定義

次にモデルの操作を行うコントローラを定義します。 AngularJS ではモデルとビューの双方向バインディングという機能があり、 モデルを変更すればビューも変更され、ビューを変更すればモデルも変更されるようになっています。 双方向バインディングは $scope オブジェクトを通じて行なわれます。 例えばアプリの設定を管理する conf モデルがあったとします。

app.controller('TaskCtrl', function($scope){
    $scope.conf = {
        appName : 'TODO'
    }
});


この conf モデルの値をビューにバインドするには、 HTML 上のコントローラの機能を有効にする範囲に ng-controller 属性を記述し、モデルの値をバインドしたい箇所に {{属性名}} と記述します。

<div ng-controller="TaskCtrl">
    <h1>{{conf.appName}}</h1> <!-- TODO と表示される-->
</div>

これで h1 要素に TODO と表示されます。モデルの値を変更するとリアルタイムに画面上の出力も変更されます。 (コントローラー内に別のコントローラーを定義した場合、親コントローラーに定義された属性値を $scope 経由で参照することができます。)

DEMO

フォームとモデルのバインド

HTML のフォーム要素にモデルの値やメソッドを割り当てることもできます。 タスクの登録フォームを設置し、未入力の状態で登録しようとした場合、エラー表示するようにしてみます。

<form ng-submit="addTask()">
    <input ng-model="taskTitle" placeholder="タスクを入力してください">
    <span>{{error}}</span>
</form>


$scope に addTask, taskTitle, error を追加することでビューとバインドされ、 何も入力せずエンターした場合はエラーが表示されます。

$scope.taskTitle = '';
$scope.addTask = function(){
    if(!$scope.taskTitle.length){
        $scope.error = 'タスクが入力されてません!'
        return;
    }
    $scope.error = '';
}

demo

DEMO

コレクションの繰り返し処理

登録したタスクを配列に格納することで、リスト形式で出力させることができます。 前述の addTask() メソッドにてモデルを配列に追加していきます。

//配列の定義
$scope.tasks = [];
$scope.addTask = function(){
    ・・・
    //モデルの追加
    $scope.tasks.push({
        title: $scope.taskTitle,
        closed: false
    });
    //入力フィールドのクリア
    $scope.taskTitle = '';
}


画面に表示する際は、ng-repeat を使用します。

<ul class="taskList">
    <li ng-repeat="task in tasks" class="closed-{{task.closed}}">
        <input type="checkbox" ng-model="task.closed">
        <span class="title">{{task.title}}</span>
    </li>
</ul>


li のクラスに closed-{{task.closed}} と割り振っているので、下記スタイルの適用で完了タスクに取消線が引かれます。

li.closed-true .title{
    color: gray;
    text-decoration: line-through;
}

demo

DEMO

選択したタスクの削除

ng-repeat 内で $index を参照すると配列番号を取得することができます。 これを利用し選択したタスクを削除する機能を追加してみます。 また、クリックイベントにて preventDefault() を行うために $event も参照します。

<ul class="taskList">
    <li ng-repeat="task in tasks" class="closed-{{task.closed}}">
        <input type="checkbox" ng-model="task.closed">
        <span class="title">{{task.title}}</span>
        <!-- delTaskに$event, $indexを渡す -->
        <a ng-click="delTask($event, $index)" href="#">x</a>
    </li>
</ul>


delTask() メソッドに $index を渡すことで、指定されたタスク番号を配列から削除することができます。

$scope.delTask = function($event, $index){
    $event.preventDefault();
    $scope.tasks.splice($index, 1)
}


ついでにタスクを全て削除するボタンも設置してみます。

<button class="clear" ng-click="delAllTask()">全て削除</button>


配列の中身を空にすることで全て削除されます。

$scope.delAllTask = function(){
    $scope.tasks = [];
}

demo

DEMO

TODO アプリの完成

とりあえずここまでで TODO アプリの機能としては完成とします。 ソースは以下のようになります。

code

css
li.closed-true .title{
    color: gray;
    text-decoration: line-through;
}
html
<div ng-app="taskApp">
    <div ng-controller="TaskCtrl">
        <h1>{{conf.appName}}</h1>
        <form ng-submit="addTask()">
            <input ng-model="taskTitle" placeholder="タスクを入力してください">
            <span>{{error}}</span>
        </form>
        <ul class="taskList">
            <li ng-repeat="task in tasks" class="closed-{{task.closed}}">
                <input type="checkbox" ng-model="task.closed">
                <span class="title">{{task.title}}</span>
                <a class="del" ng-click="delTask($event, $index)" href="#">x</a>
            </li>
        </ul>
        <button class="clear" ng-click="delAllTask()">全て削除</button>
    </div>
</div>
script
angular.module('taskApp',[]).
    controller('TaskCtrl', function($scope){
        $scope.conf = {
            appName : 'TODO'
        }
        $scope.taskTitle = '';
        $scope.tasks = [];
        $scope.addTask = function(){
            if(!$scope.taskTitle.length){
                $scope.error = 'タスクが入力されてません!'
                return;
            }
            $scope.tasks.push({
                title: $scope.taskTitle,
                closed: false
            });
            $scope.taskTitle = '';
            $scope.error = '';
        }
        $scope.delTask = function($event, $index){
            $event.preventDefault();
            $scope.tasks.splice($index, 1)
        }
        $scope.delAllTask = function(){
            if(confirm('タスクを全て削除しますか?')){
                $scope.tasks = [];
            }
        }
    });

外部テンプレートの読み込み

テンプレートを読み込んだり、DOM に処理を追加する機能としてディレクティブといものがあります。 ここでは独自のディレクティブを定義し、外部テンプレートを読み込んでみます。 画面の上部、下部それぞれに以下の tmpl-bar.html を読み込んでみます。

<!-- tmpl-bar.html -->
<div class="bar">{{bar.title}}</div>


テンプレートを出力したい箇所に、独自定義のディレクティブ名 tmpl-bar(ハイフン区切りで表記)を記述します。

<div tmpl-bar ng-controller="BarCtrl" data="top"></div>
<div ng-controller="TaskCtrl">・・・</div>
<div tmpl-bar ng-controller="BarCtrl" data="bottom"></div>


directive() にて、テンプレートファイルとディレクティブ名のマッチングを行います。

angular.module('taskApp', []).
    controller('TaskCtrl', function($scope){
        ・・・
    }).
    controller('BarCtrl', function($scope){
        $scope.top = {title:'HEAD'}
        $scope.bottom = {title:'FOOT'}
    }).
    directive('tmplBar', function() {
        return {
            scope: {
                bar: '=data'
            },
            templateUrl: "tmpl-bar.html"
        };
    });

demo

DEMO

モジュールの再利用とサービス化

再利用したい処理は別モジュールで管理し、必要に応じ読み込んで利用できます。 また、汎用的な処理はサービスとして定義できます。 例えば以下のように認証処理のモックを別モジュールのサービスとして定義してみます。

angular.module('auth',[]).
    service('authService', function(){
        return {
            user : '',
            label : 'LOGIN',
            toggle : function(){
                if(this.label == 'LOGIN'){
                    this.label = 'LOGOUT';
                    this.user = 'Taro Yamada';
                }
                else{
                    this.label = 'LOGIN';
                    this.user = '';
                }
            }
        }
    });


このサービスを利用するには、以下のように記述します。

angular.module('taskApp',['auth']). // authモジュールの読み込み
    controller('AuthCtrl', function($scope, authService){ // authServiceの取得
            $scope.auth = authService;
    }).
    controller('TaskCtrl', function($scope){
        alert($scope.auth); //undefined ※参照NG
    });

但し、上記の記述のままだと AuthCtrl で定義された $scope.auth を TaskCtrl 内から参照できません。

以下のように authService を $rootScope に割り当てることで、他のコントローラからも参照可能になります。

angular.module('taskApp',['auth']). // authモジュールの読み込み
    controller('AuthCtrl',function($rootScope, authService){ // authServiceの取得
        $rootScope.auth = authService;
    }).
    controller('TaskCtrl', function($scope){
        alert($scope.auth); //[object Object] ※参照OK
    });


または、TaskCtrl で authService を取得することで、同一の authService オブジェクトを参照することができます。

angular.module('taskApp',['auth']). // authモジュールの読み込み
    controller('AuthCtrl',function($scope, authService){ // authServiceの取得
        $scope.auth = authService;
    }).
    controller('TaskCtrl', function($scope, authService){ // authServiceの取得
        alert(authService); //[object Object] ※参照OK
    })


以下のようなマークアップを追加することで、認証 UI が表示されます。

<div ng-app="taskApp">
    <div ng-controller="AuthCtrl">
        <button ng-click="auth.toggle()">{{auth.label}}</button> {{auth.user}} 
    </div>
    ・・・

demo

DEMO

サービスとしての外部テンプレート機能

前述の定義方法だと認証サービスと外部テンプレートのバインドを、自前でコントローラを用意して行う必要がありました。 サービスモジュール内でこれらのバインド処理を行い、呼び出し側の記述をシンプルにしてみます。

ファクトリ関数内で link 属性に関数を記述することで、scope を参照できるので、ここでバインドします。

angular.module('auth', []).
    service('authService', function(){
        ・・・
    }).
    directive('tmplAuth', function(authService) { // 認証サービスの読み込み
        return {
            templateUrl: "tmpl-auth.html",
            link : function(scope, element, attrs){ // scope を参照できる
                scope.auth = authService;
            }
        };
    });


これによりサービスを利用する側は、サービスのモジュールを読み込むのみでよく、コントローラの定義も $scope へのバインドも不要になります。

angular.module('taskApp',['auth']) // authモジュールの読み込み


HTML にディレクティブ属性を記述すれば、テンプレートが埋め込まれます。

<div tmpl-auth></div>

DEMO

最後に

こうやってまとめるのもなかなか骨がおれますね・・^^;
最小のアプリを作る上で最低限おさえておきたい機能はどの辺かなぁ、という視点で調べたとこをまとめてみました。 最初はドットインストールさんの動画が分かりやすくて良いかもしれません。 こちらに動画を見ながらタイプしたコードがありますのでよろしければどうぞ。

AngularJS で検索すると結構な数の日本語記事があり注目されているのが良く分かります。 機能も豊富なようで覚えることだらけとの事で、 趣味の範囲で習得するのは大変そうですが仕事でこれを扱えたらやりがいがありそうですね!(うらやましい・・)