AngularJS1.5でComponentを使ってみる

## Componentとは

AngularJS1.5になって新しく`component()`が使えるようになりました。
これはAngularJS1.4以前から存在しているお馴染みの`directive()`に（いずれは）取って代わる存在で、今後主流になるであろうAngularJS2への移行がよりスムーズになるよう設けられたメソッドです。

AngularJS1.5時点での`component()`は特定の設定で呼び出した`directive()`のラッパーメソッドとなっているので、`directive()`を用いても同様なカスタム要素を作成することが出来ます。

が、折角なので`component()`を用いてなにか作ってみます。

## 作るもの

SPAのチュートリアルとしてド定番なTODOアプリをサクッと作ります。
また今回はサーバーサイドの実装を行わないので、mockを用いてサーバーサイドの挙動をシミュレーションします。

## 仕様

- 初期表示時に通信して既に登録されているタスクリストを取得してくる。
- タスクの追加、削除が行える

## ディレクトリ構成

こんな感じ。

```text:directory
.
├── todoApp
│   ├── components
│   │   └── todoList.js
│   ├── controllers
│   │   └── todoListController.js
│   ├── factories
│   │   └── tasksResource.js
│   ├── app.js
│   └── mock.js
└── index.html
```

各ファイルの役割は下記のとおりです。

- index.html
  - 各ファイルの読み込みとtodoListコンポーネントの呼び出し

- app.js
  - 各種モジュールの宣言とangular本体のbootstrap

- mock.js
  - サーバーサイドの挙動を定義

- todoList.js
  - todoListコンポーネント本体

- todoListController.js
  - todoListコンポーネントで利用するコントローラー

- taskResource.js
  - サーバーサイドとの通信を行うためのサービス（厳密にはFactory）

## 実装

では各ファイルの中身を書きます。


### app.js

```js:app.js
void function() {

    // todoAppを定義、必要なサービスを注入
    angular.module('todoApp', [
        // $resourceを利用可能に
        'ngResource',
        // $httpBackendを利用可能に
        'ngMockE2E',
        'controllers',
        'factories',
        'components'
    ]);

    // 作成したコントローラーの登録先
    angular.module('controllers', []);
    // 作成したファクトリーの登録先
    angular.module('factories', []);
    // 作成したコンポーネントの登録先
    angular.module('components', []);

    // ページロード完了後、angularを起動（todoAppをdocumentへ適用）
    document.addEventListener('DOMContentLoaded', function() {
        angular.bootstrap(document, ['todoApp']);
    });

}();
```

`controllers`, `factories`, `components`モジュールを作成し、それを`todoApp`モジュールへ注入することで、`todoApp`に対して直接controllerなどを生成することを回避しています。

### mock.js

```js:mock.js
void function () {

    // runフェーズにて実行させる処理
    var run = function($httpBackend) {

        // 初回タスクロード時に返却するデータを定義
        var tasks = [
            {
                id: 1,
                title: '買い物'
            },
            {
                id: 2,
                title: '勉強'
            },
            {
                id: 3,
                title: '掃除'
            }
        ];

        // 各種リクエストが来た際のレスポンスを定義
        $httpBackend.whenGET('/api/tasks').respond(tasks);
        $httpBackend.whenPOST('/api/tasks').respond();
        $httpBackend.whenDELETE(/\/api\/tasks\/\d+/).respond();
    };

    // runフェーズの処理を登録
    angular
        .module('todoApp')
        .run([
            '$httpBackend',
            run
        ]);

}();
```

`$httpBackend`の`whenGET`, `whenPOST`, `whenDELETE`はその名の通り、`GET`, `POST`, `DELETE`リクエストを行なった際の挙動を定義できます。`$http`や`$resource`を通じて行うXHRのURIと`$httpBackend`に登録されているURIとが一致した場合、`respond()`へ渡した値を返却するようになります。
`respond()`に何も渡さなかった場合は、リクエスト時のpayload（送信したデータ）がそのまま返るようになります。

### todoList.js

```js:todoList.js
void function() {

    // コンポーネントのテンプレートを定義
    var template = [
        '<input type="text" ng-model="$ctrl.data.text">',
        '<button type="button" ng-click="$ctrl.addTask()">追加</button>',
        '<ul>',
            '<li ng-repeat="item in $ctrl.items">',
                '<span ng-bind="item.title"></span>',
                '<button ng-click="$ctrl.removeTask($index)">削除</button>',
            '</li>',
        '</ul>'
    ];

    // todoListコンポーネントを定義
    var todoList = {
        // テンプレートを指定
        template: template.join(''),
        // コントローラーを指定
        controller: 'todoListController'
    };

    // componentsモジュールに登録
    angular
        .module('components')
        .component('todoList', todoList);

}();
```

`component()`へは直接オブジェクトを渡します。`directive()`へはオブジェクトを返却する関数を渡すので、ココが多少違う部分となります。
テンプレートは非常にシンプルで、タスク登録用の入力要素と保存ボタン、登録されたタスクを表示するためのリスト要素のみです。


### todoListController.js

```js:todoListController.js
void function () {

    /**
     * todoListコンポーネントから呼ばれるコントローラー
     * @param tasksResource
     */
    var todoListController = function(tasksResource) {

        // thisへの参照を格納しておく
        var _this = this;

        // 入力値を保持しておくための入れ物
        _this.data = {};

        // 初回起動時、DBに登録されているタスクを取得して格納しておく
        // `query()`で`/api/tasks`へのGETリクエストが走る
        _this.items = tasksResource.query();

        // タスクの追加処理
        _this.addTask = function() {

            // 入力値が空なら処理を抜ける
            if (!_this.data.text) {
                return;
            }

            // 入力値を元に新規taskResourceを生成
            var task = new tasksResource({
                title: _this.data.text
            });

            // 生成したタスクをDBへ登録
            // `$save()`で`/api/tasks`へのPOSTリクエストが走る
            task.$save(function(response) {
                // IDが発行されたと仮定して付与
                response.id = _this.items.length + 1;
                // レスポンスをそのままリスト配列の最後尾へ追加
                _this.items.push(response);
            });

            // 入力値をクリアする
            clearText();
        };

        // タスクの削除処理
        _this.removeTask = function($index) {
            // サーバーへDELETEリクエストを送信
            // `$remove()`で`/api/tasks/:id`へのDELETEリクエストが走る
            _this.items[$index].$remove(function() {
                // 配列中から対象のタスクを削除
                _this.items.splice($index, 1);
            });
        };

        // 入力値のクリア用関数
        var clearText = function() {
            delete _this.data.text;
        };

    };

    // controllerモジュールへ登録
    angular
        .module('controllers')
        .controller('todoListController', [
            'tasksResource',
            todoListController
        ]);

}();
```

todoListコンポーネントのメイン処理部分です。コンポーネントが初期化されるとサーバーへのリクエストが走り、既に登録されているタスクを取得しにいきます。今回はmockを準備しているのでそこからから3件のタスク（配列）が返却され、ビューにレンダリングされます。
また`addTask()`はタスク追加用、`removeTask()`はタスク削除用のメソッドです。

### taskResource.js

```js:taskResource.js
void function() {

    /**
     * タスク取得、追加、削除の歳に利用されるリソースオブジェクト
     * @param $resource
     * @returns Resource
     */
    var tasksResource = function($resource) {
        return $resource('/api/tasks/:id', {
            id: '@id'
        });
    };

    // factoriesモジュールへ登録
    angular
        .module('factories')
        .factory('tasksResource', [
            '$resource',
            tasksResource
        ]);

}();
```

特に複雑なことはしていません。従来通りResourceを定義しています。
$resourceの詳細は[公式リファレンス](https://docs.angularjs.org/api/ngResource/service/$resource)が非常に分かり易いです。

### index.html

```html:index.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>TodoApp</title>
</head>
<body>

    <!-- todoListコンポーネントを呼び出す -->
    <todo-list></todo-list>

    <!-- angularの必要ライブラリを読み込む -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular-resource.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular-mocks.js"></script>

    <!-- 作成したファイルを全て読み込む -->
    <script src="todoApp/app.js"></script>
    <script src="todoApp/mock.js"></script>
    <script src="todoApp/factories/tasksResource.js"></script>
    <script src="todoApp/components/todoList.js"></script>
    <script src="todoApp/controllers/todoListController.js"></script>

</body>
</html>
```

`component('todoList', ...)`として生成したコンポーネントはhtml側にて`<todo-list></todo-list>`と記述することで呼び出すことが出来ます。

## 動かす

`index.html`にアクセスします。

![171642ad204c7acd2cf9eabaf0f3a65b.png](https://qiita-image-store.s3.amazonaws.com/0/112725/bb4639f3-8ab1-6a42-89f0-ea3866ed75be.png)

とこんな感じになっているはずです。
デザインが寂しいですが、とりあえず動きます。

![611b6a1c0ab9fbdf0e7e053c8341239c.gif](https://qiita-image-store.s3.amazonaws.com/0/112725/88b2d5a2-6048-ff24-34bf-02be9305d411.gif)

## ということで

今回は5分で出来るコンポーネント指向なTODOアプリを作ってみました。今後は積極的に`component()`を用いてアプリを開発していきたいですね。
次回はこのアプリを題材にしてテストを書いてみようと思います。

それではよいAngularライフを！