Welcome to AngularJS

ディレクティブを作成して汎用的に使えるようにしよう

ユーザー登録フォームをディレクティブ化して再利用しよう〈Angularシリーズ vol.3〉

前提条件

  • HTML、JavaScriptの基本的な知識

執筆環境

  • OS X 10.10
  • AngualrJS 1.4.0

前回までは、AngualrJSの基本的な使い方として、AngularJS標準ディレクティブの使い方を紹介してきました。

ディレクティブとは、コントローラやサービスの処理を、HTMLのタグとしてView(DOM)側で使用できる仕組みです。
これまで、ng-modelng-controllerng-repeatなどを使ってきました。

今回はこのディレクティブを自作してみたいと思います。

単純なフォームを作成

下記機能を持った、単純な登録フォームを作成し、ディレクティブの扱い方を紹介したいと思います。

  • 入力項目

    • 名前(必須)
    • メールアドレス(形式チェック)
  • 機能

    • 入力に誤りがある場合、エラーを表示する
    • エラーがある場合、登録ボタンをクリックできない

ライブラリを読み込む

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- ライブラリ -->
<!DOCTYPE html>
<html ng-app="MyApp">
    <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="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

        <!-- AngularJSの処理を記述する -->
        <script type="text/javascript" src="script.js"></script>
    </head>
    <body>
    </body>
</html>

AngularJSに加えて、jQuery、Bootstrapを使います。
また、AngularJSの処理を記述するscript.jsも読み込みます。

モジュールを定義

script.js

1
2
3
4
5
6
7
8
9
angular.module("MyApp", [])
    .controller("MyController", function () {
        this.patterns = {
            mail: {
                regex: /^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/,
                message: 'メールアドレス形式が不正です。'
            }
        };
    });

MyAppという名前でモジュールを定義します。MyAppモジュールにはMyControllerという名前でコントローラを定義します。

コントローラには、フォームのメールアドレス形式をチェックする正規表現と、エラーメッセージを定義しておきます。

フォームを定義

index.html

1
2
3
4
5
6
<div class="container" ng-controller="MyController as vm">
    <h3>会員登録</h3>

    <form name="myForm">
    </form>
</div>

ng-controllerに先ほど定義したMyControllerを指定します。as vmはこのコントローラに別名をつけ参照できるようにしています。
フォームは純粋なHTMLのフォームでmyFormという名前をつけます。

名前フォーム

index.html

1
2
3
4
5
6
7
8
9
10
<form name="myForm">
    <!-- 3. エラー表示 -->
    <div class="form-group" ng-class="{'has-error': myForm.name.$dirty && myForm.name.$invalid}">
        <label class="control-label">氏名</label>*
        <!-- 2. エラーメッセージ表示 -->
        <small ng-if="myForm.name.$dirty && myForm.name.$error.required">必須です</small>
        <!-- 1. 入力フォーム -->
        <input type="text" class="form-control" name="name" ng-model="formData.name" ng-required="true" placeholder="田中太郎">
    </div>
</form>

1. 入力フォーム

ng-modelには、フォームに入力されたデータを格納するformData.nameを指定します。
名前は入力必須にするので、ng-requiredを記述します。
フォームの必須チェックをしてくれるAngularJSの標準ディレクティブです。

2. エラーメッセージ表示

名前の入力がない場合のエラーメッセージを表示します。

ng-ifは、DOMを表示するかどうかの条件を指定します。

myForm.name.$error.requiredは、必須エラーが発生しているかどうかを表します。ただし、これだけ記述している場合、画面を表示した時点ではフォームが空なので、いきなりエラーが表示されます。
そこで、myForm.name.$dirtyも合わせて指定します。これは、フォームに手が加えられたかどうかの状態を保持しているため、画面表示時はfalseになっています。

3. エラー表示

ただエラーメッセージを表示するだけだと味気ないので、エラーが発生したときにフォーム全体を赤くし、エラーをわかりやすくします。

クラス属性にhas-errorを指定すると、Bootstrapの機能が使えます。エラーが発生している場合にのみng-errorを指定するためにng-classを使用します。myForm.name.$dirty && myForm.name.$invalidは、フォームに手が加えられたかどうか、かつ名前入力フォームにエラーが発生しているかどうかという意味です。

実行してみましょう。

一度文字を入力してから、文字を消すと入力必須のエラー表示になります。

メールアドレスフォーム

index.html

1
2
3
4
5
6
7
8
9
<div class="form-group" ng-class="{'has-error': myForm.mail.$dirty && myForm.mail.$invalid}">
    <label class="control-label">メールアドレス</label>
    <!-- 2. エラーメッセージ -->
    <span ng-if="myForm.mail.$dirty && myForm.mail.$invalid">
        {{vm.patterns.mail.message}}
    </span>
    <!-- 1. 入力フォーム -->
    <input type="text" class="form-control" name="mail" ng-model="formData.mail" ng-pattern="vm.patterns.mail.regex" placeholder="example@example.com"/>
</div>

1. 入力フォーム

メールアドレスフォームです。名前フォームとほとんど同じ構造ですが、メールアドレスの形式チェックをするために、ng-patternディレクティブを使用します。コントローラで定義した正規表現を指定します。
これを指定するだけで、入力された内容が正規表現にマッチするかのチェックが実行されます。

2. エラーメッセージ

フォームに手が加えられたかどうか、かつメールアドレスフォームにエラーが発生している場合に、コントローラで定義したエラーメッセージが表示されます。

実行してみましょう。

正しいメールアドレス形式でない場合、エラーが表示されます。

登録ボタン

最後にボタンを定義します。

index.html

1
<button class="button btn btn-primary btn-block" ng-disabled="myForm.$invalid">登録</button>

ng-disabled属性にmyForm.$invalidを指定します。フォームにエラーがある場合は、ボタンをクリックできないようになります。

実行してみましょう。

エラーが発生している場合は、登録ボタンがクリックできないようになっています。

HTMLファイルが肥大化する

ここまでの手順で、名前とメールアドレスを入力でき、エラーに対応したフォームを実現することができました。
しかし、簡潔なフォーム1つつくるだけで、以下のようにHTMLファイルが肥大化しています。

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
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html ng-app="MyApp">
    <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="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

        <!-- AngularJSの処理を記述する -->
        <script type="text/javascript" src="script.js"></script>
    </head>
    <body>
        <div class="container" ng-controller="MyController as vm">
            <h3>会員登録</h3>

            <form name="myForm">
                <!-- 3. エラー表示 -->
                <div class="form-group" ng-class="{'has-error': myForm.name.$dirty && myForm.name.$invalid}">
                    <label class="control-label">氏名</label>*
                    <!-- 2. エラーメッセージ表示 -->
                    <small ng-if="myForm.name.$dirty && myForm.name.$error.required">必須です</small>
                    <!-- 1. 入力フォーム -->
                    <input type="text" class="form-control" name="name" ng-model="formData.name" ng-required="true" placeholder="田中太郎">
                </div>
                <div class="form-group" ng-class="{'has-error': myForm.mail.$dirty && myForm.mail.$invalid}">
                    <label class="control-label">メールアドレス</label>
                    <!-- 2. エラーメッセージ -->
                    <span ng-if="myForm.mail.$dirty && myForm.mail.$invalid">
                        {{vm.patterns.mail.message}}
                    </span>
                    <!-- 1. 入力フォーム -->
                    <input type="text" class="form-control" name="mail" ng-model="formData.mail" ng-pattern="vm.patterns.mail.regex" placeholder="example@example.com"/>
                </div>
                <button class="button btn btn-primary btn-block" ng-disabled="myForm.$invalid">登録</button>
            </form>
        </div>
    </body>
</html>

今後、フォーム要素を増やす場合、記述している内容がどんどん肥大化し、複雑で可読性が落ちていくでしょう。
そこで、再利用可能なフォーム用のディレクティブをつくって、HTMLファイルを簡潔に記述できるようリファクタリングしてみます。

ディレクティブを作成

以下は、ローカル環境では実行できないことがあります。サーバにアップロードしてお試しください。

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
angular.module("MyApp", [])
    .controller("MyController", function () {
        this.patterns = {
            mail: {
                regex: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
                message: 'メールアドレス形式が不正です。'
            }
        };
    })
    .directive('myInput', function () {
        return {
            restrict: 'E',
            require: '^form',
            replace: true,
            scope: {
                title: '@',
                name: '@',
                placeholder: '@',
                ngModel: '=',
                required: '=',
                validation: '='
            },
            link: function (scope, element, attrs, controller) {
                var target = controller[attrs.name];

                scope.isError = function () {
                    return target.$dirty && target.$invalid;
                };

                scope.isRequiredError = function () {
                    return target.$error.required;
                };

                scope.isInvalidError = function () {
                    return !scope.isRequiredError()
                        && target.$invalid
                        && scope.validation.message;
                };
            },
            templateUrl: 'template/template.html'
        };
    });

ディレクティブはこのようにdirective()の第1引数にディレクティブ名、第2引数にディレクティブの定義を記述します。
これによって、下記のようなカスタムHTMLタグが使用できます。

ディレクティブ名は、定義時はキャメルケース(myInput)、使用時はハイフン区切り(my-input)になります。

index.html

1
<my-input></my-input>

ディレクティブの定義によって、HTMLの記述をディレクティブの中に隠蔽することができます。

restrict

ディレクティブをHTML要素として使用するためにrestrict: 'E'を指定します。「E」はelementの略です。
一方、ディレクティブを属性として使用するためには、restrict: 'A'(A = Attribute)を指定します。Aを指定した場合は、下記のように使用できます。

index.html

1
<div my-input></div>

require

指定した対象が存在しない場合にエラーを出力します。
^は親階層という意味で、今回の記述だと親階層にformが必要、つまりこのディレクティブはformの子要素として使用する、という制約になります。

formの子として記述しない場合、下記のようなエラーが出力されます。

replace

<my-input></my-input>と記述するとこのタグがHTML上に残りますが、replace: trueを指定すると、それが残りません。

scope

スコープの定義です。scopeにハッシュを指定すると、ディレクティブ内独自のスコープが定義できます。
@はstring値、=はデータバインディングの意味です。

link

scopeの値を処理してテンプレート部分渡したい場合は、linkに処理を記述します。

第4引数のcontrollerは親要素のformオブジェクトになり、controller[attrs.name]では<my-input></my-input>自身のオブジェクトが取得できます。

また、HTMLに記述していたエラーかどうか判断する処理を、ディレクティブのscopeに定義しています。
例えば、scope.isError = function ()のように定義すると、テンプレートでisError()で定義した処理を実行できます。

templateUrl

template.html

1
2
3
4
5
6
7
8
9
<div class="form-group" ng-class="{'has-error': isError()}">
    <label ng-bind="title" class="control-label"></label><span ng-if="required">*</span>

    <small ng-show="isError()">
        <span ng-if="isRequiredError()">必須です</span>
        <span ng-if="isInvalidError()">{{validation.message}}</span>
    </small>
    <input type="text" class="form-control" name="{{name}}" ng-model="ngModel" ng-required="required" ng-pattern="validation.regex" placeholder="{{placeholder}}">
</div>

最後に、テンプレート部分の記述です。template.htmlはtemplateディレクトリに置いているため、"template/template.html"を指定しています。
ディレクティブのscopeに定義した関数や値を使って、汎用的なHTMLを定義しています。

※ここに記述したのは、ディレクティブの定義の一部分のみです。 詳細は公式ドキュメントを参照してください。

ディレクティブを使う

index.html

1
2
3
4
5
6
7
8
9
10
11
12
<div class="container" ng-controller="MyController as vm">
    <h3>会員登録</h3>

    <form name="myForm">
        <my-input title="氏名" name="name" ng-model="formData.name"
             required="true" placeholder="田中太郎"></my-input>
        <my-input title="メールアドレス" name="mail" ng-model="formData.mail"
             placeholder="example@example.com" validation="vm.patterns.mail"></my-input>

        <button class="button btn btn-primary btn-block" ng-disabled="myForm.$invalid">登録</button>
    </form>
</div>

作成したディレクティブは、<my-input></my-input>で使用で、タグの属性はscopeで定義したものが指定できます。

実行してみましょう。

まとめ

ディレクティブを作成することで、コードがかなり完結に記述でき見通しがよくなりました。AngularJSを使い始めの頃は、上記のようにHTMLファイルが肥大化したり、controllerにいろいろ詰め込みすぎてしまうことでコードが複雑化してしまいがちです。このような状況に陥ったら、ディレクティブを作ってみることをお勧めします。

Tech Blog

(編集部)

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

NEXT