Welcome to AngularJS

カスタムフィルタを実装して、ToDoアプリを拡張しよう

カスタムフィルターで実現する複数ワード検索機能〈Angularシリーズ vol.2〉

前提条件

  • HTML、JavaScriptの基本的な知識

執筆環境

  • OS X 10.10
  • AngualrJS 1.4.0

ToDoアプリのおさらい

今回は、前回作ったToDoアプリを拡張してみます。

  1. ToDoの一覧表示
  2. ToDoの新規作成
  3. 完了したToDoのチェック
  4. 完了したToDoのアーカイブ
  5. ToDoの検索

これらの機能を持つ、アプリの構成は下記の通りでした。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html ng-app="ToDo">
<head>
    <meta lang="ja" charset="UTF-8">
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
    <script type="text/javascript" src="script.js"></script>
    <link type="text/css" rel="stylesheet" href="style.css">
    <title>ToDo</title>
</head>
<body>
<div ng-controller="TodoController as vm">
    <form ng-submit="vm.create()">
        <input type="text" ng-model="vm.newTodo" placeholder="ToDo名を入力">
        <input type="submit" type="button" value="新規作成">
        <input type="button" ng-click="vm.archive()" value="完了したToDoをアーカイブ">
    </form>
    <form>
        <input type="text" ng-model="vm.keyword" placeholder="ToDoを検索">
    </form>
    <ul ng-repeat="todo in vm.todos | filter:vm.keyword">
        <li>
            <input type="checkbox" ng-model="todo.done">
            <span ng-class="{'done': todo.done }">{{todo.title}}</span>
        </li>
    </ul>
</div>
</body>
</html>

style.css

1
2
3
4
.done {
    text-decoration: line-through;
    color: gray;
}

script.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
angular.module('ToDo', [])
    .controller('TodoController', function () {
        var self = this;

        self.todos = [
            {title: 'システム企画', done: true},
            {title: '要件定義', done: false}
        ];

        self.create = function () {
            self.todos.push({title: self.newTodo, done: false});
            self.newTodo = '';
        };

        self.archive = function () {
            var currentTodo = self.todos;
            self.todos = [];
            angular.forEach(currentTodo, function (todo) {
                if(!todo.done) {
                    self.todos.push(todo);
                }
            });
        };
    });

検索フォームで文字を入力すると、表示されるToDoが絞り込まれました。

これは、ng-repeatで描画している配列をfilter:vm.keywordでフィルタリングすることで実現しています。vm.keywordが検索フォームのng-model="vm.keyword"と連動していて、対象が絞り込まれています。

今回は、カスタムフィルターを自作して、複数ワードで検索できる機能を追加してみます。 Google検索のように、スペース区切りで入力したワードにヒットするToDoを表示できるようにします。

ディレクトリ構成を変更

カスタムフィルタ実装の前に、script.jsのコード量が増えるので、分離します。

TodoController.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function TodoController() {
    var self = this;

    self.todos = [
        {title: 'システム企画', done: true},
        {title: '要件定義', done: false}
    ];

    self.create = function () {
        self.todos.push({title: self.newTodo, done: false});
        self.newTodo = '';
    };

    self.archive = function () {
        var currentTodo = self.todos;
        self.todos = [];
        angular.forEach(currentTodo, function (todo) {
            if (!todo.done) {
                self.todos.push(todo);
            }
        });
    };
}

script.js

1
2
angular.module('ToDo', [])
    .controller('TodoController', TodoController);

.controllerの第二引数に記述していた関数をTodoControllerという名前をつけた関数に切り出します。HTML内でng-controllerに指定する名前は、.controllerの第1引数に指定しているTodoControllerを使用します。

カスタムフィルタを実装

カスタムフィルタの実装は簡単です。 公式ドキュメントに則ったフォーマットでフィルタ用の関数を定義してモジュールに登録します。

カスタムフィルタの関数を定義

CustomFilter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function CustomFilter(todoService) {
    return function (list, searchQuery) {
        if (searchQuery) {
            // 全角スペースを半角スペースに置換
            var query = searchQuery.replace(/ /g, " ");
        }

        // 検索フォームに文字が入力されている場合
        if (query) {
            // 検索対象ワードの配列を作成
            var queryWordArray = query.split(" ");

            var filteredList = [];

            list.forEach(function (obj) {
                // 検索キーワードでオブジェクトを探索
                var isMatch = !queryWordArray.some(function (keyword) {
                    return !todoService.keywordJudge(obj, keyword);
                });

                // 検索キーワードがAND一致した場合、一覧に表示する配列に格納
                if (isMatch) {
                    filteredList.push(obj);
                }
            });
            return filteredList;
        }
        return list;
    };
}

関数CustomFilterは、ng-repeatで描画しているリストにフィルタをかけて返します。 第1引数listng-repeatに指定しているtodos、第2引数searchQueryがフィルタの元となるクエリ文字列です。

クエリ文字列が指定されていない場合は、フィルタをかけずそのまま返します。 クエリ文字列をスペース区切りで配列化し、それぞれの値とlistを比較して一致するかどうかを判別します。一致したオブジェクトのみを配列filteredListに格納して返します。

一致判別をする処理はサービスとして定義してみます。 関数CustomFilterの引数に指定しているtodoServiceがサービスのインスタンスでtodoService.keywordJudgeは、この後TodoServiceに定義します。

サービスを定義

TodoService.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function TodoService() {
}

TodoService.prototype.keywordJudge = function (obj, keyword) {
    var self = this;

    if (angular.isArray(obj)) {
        // 配列の場合
        return obj.some(function (child) {
            return self.keywordJudge(child, keyword);
        });
    } else if (angular.isObject(obj)) {
        // オブジェクトの場合
        var properties = Object.getOwnPropertyNames(obj);
        return properties.some(function (property) {
            var child = obj[property];
            return self.keywordJudge(child, keyword);
        });
    } else if (obj != null) {
        // オブジェクト、配列以外で、値がある場合
        return angular.toJson(obj).search(keyword) != -1;
    }
    // nullまたはundefinedの場合
    return false;
};

CustomFilterで使用される関数todoService.keywordJudgeを定義します。 配列の場合は、引数で渡されたオブジェクトでキーワードに一致する対象があるかどうかチェックし一致する対象があればそれを返します。 オブジェクトの場合は、子要素を順番にチェックし、ひとつでも部分一致した場合trueを返します。 さらに、配列・オブジェクト以外の場合は文字列に変換し、一致判定します。

フィルタとサービスを登録

script.js

1
2
3
4
angular.module('ToDo', [])
    .controller('TodoController', TodoController)
    .service('todoService', TodoService)
    .filter('customFilter', CustomFilter);

作成したフィルタとサービスをモジュールに登録します。コントローラと同様に、第1引数に指定しているtodoServicecustomFilterによって、登録した処理を参照します。

ファイルを読み込み

index.htmlに実装したファイルを読み込みます。

1
2
3
4
<script type="text/javascript" src="CustomFilter.js"></script>
<script type="text/javascript" src="TodoService.js"></script>
<script type="text/javascript" src="TodoController.js"></script>
<script type="text/javascript" src="script.js"></script>

フィルタを適用

最後に実装したフィルタを適用します。filtercustomFilterに変更します。

1
<ul ng-repeat="todo in vm.todos | customFilter:vm.keyword">

ここまで完成すると、ファイルは以下のようになります。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html ng-app="ToDo">
<head>
    <meta lang="ja" charset="UTF-8">
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
    <script type="text/javascript" src="CustomFilter.js"></script>
    <script type="text/javascript" src="TodoService.js"></script>
    <script type="text/javascript" src="TodoController.js"></script>
    <script type="text/javascript" src="script.js"></script>
    <link type="text/css" rel="stylesheet" href="style.css">
    <title>ToDo</title>
</head>
<body>
<div ng-controller="TodoController as vm">
    <form ng-submit="vm.create()">
        <input type="text" ng-model="vm.newTodo" placeholder="ToDo名を入力">
        <input type="submit" type="button" value="新規作成">
        <input type="button" ng-click="vm.archive()" value="完了したToDoをアーカイブ">
    </form>
    <form>
        <input type="text" ng-model="vm.keyword" placeholder="ToDoを検索">
    </form>
    <ul ng-repeat="todo in vm.todos | customFilter:vm.keyword">
        <li>
            <input type="checkbox" ng-model="todo.done">
            <span ng-class="{'done': todo.done }">{{todo.title}}</span>
        </li>
    </ul>
</div>
</body>
</html>

style.css

1
2
3
4
.done {
    text-decoration: line-through;
    color: gray;
}

script.js

1
2
3
4
angular.module('ToDo', [])
    .controller('TodoController', TodoController)
    .service('todoService', TodoService)
    .filter('customFilter', CustomFilter);

index.htmlをブラウザで表示してみましょう。 スペース区切りで入力した文字に一致する対象が表示されています。

まとめ

今回は、カスタムフィルターを実装してToDoアプリを拡張してみました。 AngularJSは制約が強いフレームワークですが、その制約の則れば、今回のように機能を拡張できます。

次回は、AngularJSの真骨頂であるディレクティブにフォーカスしてみます。ディレクティブはコントローラやサービスの処理を、HTMLのタグとしてView(DOM)側で使用できる仕組みです。ToDoアプリではng-modelng-controllerng-repeatなどAngularJSの標準ディレクティブを使用しています。 これらのディレクティブは、フィルタと同様に自作することができます。ディレクティブを自作しながら、AngularJSの肝を実感してみたいと思います。

Tech Blog

(編集部)

株式会社リクルートライフスタイルのTech Blog編集部です。いま流行りのTechネタやちょっと使えるTipsなどをお届けしていきます。

NEXT