フォーム

入力要素(input、select、textarea)は、ユーザーにデータを入力してもらうための手段です。 フォームは、関連する入力要素をグループ化するための入力要素の集合です。

Angularはフォームと入力要素の検証サービスを提供するため、ユーザーに不正なデータを気づかせることが可能になっています。 この機能は、ユーザーに対してどのようなエラーなのかが即座に返されるため、より快適なユーザー体験を提供します。 快適なユーザー体験を提供する役割を担うクライアントサイドの検証は、 容易に迂回出来てしまうため、送られてくる値は信用出来ないという事を忘れないで下さい。 サーバーサイドでの検証は、セキュアなアプリケーション構築のため必須です。

シンプルなフォーム

双方向のデータバインディングを認識させる上で重要になるディレクティブがngModelです。 ngModelディレクティブは、ビューからモデル、またモデルからビューを同期させる双方向のデータバインディングを提供します。 更に、その動作を強化するために他のディレクティブ用のAPIを提供します。

<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form novalidate class="simple-form">
        Name: <input type="text" ng-model="user.name" /><br />
        E-mail: <input type="email" ng-model="user.email" /><br />
        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />
        <button ng-click="reset()">RESET</button>
        <button ng-click="update(user)">SAVE</button>
      </form>
      <pre>form = {{user | json}}</pre>
      <pre>master = {{master | json}}</pre>
    </div>
  </body>
</html>
function Controller($scope) {
  $scope.master= {};

  $scope.update = function(user) {
    $scope.master= angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}
<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script>function Controller($scope) {
  $scope.master= {};

  $scope.update = function(user) {
    $scope.master= angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}
</script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form novalidate class="simple-form">
        Name: <input type="text" ng-model="user.name" /><br />
        E-mail: <input type="email" ng-model="user.email" /><br />
        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />
        <button ng-click="reset()">RESET</button>
        <button ng-click="update(user)">SAVE</button>
      </form>
      <pre>form = {{user | json}}</pre>
      <pre>master = {{master | json}}</pre>
    </div>
  </body>
</html>

novalidateは、ブラウザの本来の検証機能を無効化するのに使用させるという事に注意してください。

CSSクラスの使用

入力要素とフォームのスタイルを行えるように、ngModelは下記のCSS用のクラスを追加します。

  • ng-valid
  • ng-invalid
  • ng-pristine
  • ng-dirty

下記の例では、各フォーム入力要素の検証結果を見た目で分かるようにするためにCSSが使用されています。 この例では、user.nameuser.emailは必須となっており、 入力に不備がある場合のみ背景が赤く描画されます。 これにより、ユーザーが全て入力して確定した後に検証されるのを待つことなく、 入力しながら即座に不正な入力では無いか知ることが出来ます。

フォームへの紐付けと状態の制御

フォームは、FormControllerコントローラーのインスタンスです。 フォームのインスタンスは必要に応じて、name属性を使用してスコープに公開できます。 同様に入力要素は、NgModelControllerのインスタンスです。 コントロールインスタンスは、同様にname属性を使用してフォームインスタンスに公開することができます。 これは、フォームと入力要素の両方の内部状態は、標準のプリミティブの紐付けを使用して、 ビューへ結合するのに使用可能であることを意味します。

これにより上記の例に、下記の機能を拡張することが可能になります。

  • フォームに何らかの変更があた場合のみ、リセットボタンを有効にします。
  • フォームに入力されて値が正常である場合のみ、保存ボタンを有効にします。
  • user.emailuser.agreeのための、エラーメッセージをカスタムします。
<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        Name:
          <input type="text" ng-model="user.name" name="uName" required /><br />
        E-mail:
          <input type="email" ng-model="user.email" name="uEmail" required/><br />
        <div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
          <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
          <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
        </div>

        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />

        <input type="checkbox" ng-model="user.agree" name="userAgree" required />
        I agree: <input ng-show="user.agree" type="text" ng-model="user.agreeSign"
                  required /><br />
        <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>

        <button ng-click="reset()" ng-disabled="isUnchanged(user)">RESET</button>
        <button ng-click="update(user)"
                ng-disabled="form.$invalid || isUnchanged(user)">SAVE</button>
      </form>
    </div>
  </body>
</html>
function Controller($scope) {
  $scope.master = {};

  $scope.update = function(user) {
    $scope.master = angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.isUnchanged = function(user) {
    return angular.equals(user, $scope.master);
  };

  $scope.reset();
}
<!doctype html>
<html ng-app>
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script>function Controller($scope) {
  $scope.master = {};

  $scope.update = function(user) {
    $scope.master = angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.isUnchanged = function(user) {
    return angular.equals(user, $scope.master);
  };

  $scope.reset();
}
</script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        Name:
          <input type="text" ng-model="user.name" name="uName" required /><br />
        E-mail:
          <input type="email" ng-model="user.email" name="uEmail" required/><br />
        <div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
          <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
          <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
        </div>

        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />

        <input type="checkbox" ng-model="user.agree" name="userAgree" required />
        I agree: <input ng-show="user.agree" type="text" ng-model="user.agreeSign"
                  required /><br />
        <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>

        <button ng-click="reset()" ng-disabled="isUnchanged(user)">RESET</button>
        <button ng-click="update(user)"
                ng-disabled="form.$invalid || isUnchanged(user)">SAVE</button>
      </form>
    </div>
  </body>
</html>

検証のカスタマイズ

Angularは、HTML5のほとんどで共通するinput型(text、number、url、email、radio、checkbox)のために、 ディレクティブのための同様の検証(required、pattern、minlength、maxlength、min、max)の基本的な機能を提供します。

ngModelコントローラーにカスタム検証機能を追加した開発者自身のディレクティブを定義することによって、 開発者自身の検証を定義することが可能です。 コントローラーを保持するために、ディレクティブは下記の例のように依存関係を指定します。 検証は2箇所で行うことが可能です。

モデルからビューへの更新
紐付けたモデルが変更する度に、NgModelController#$formatters配列の全ての機能がパイプライン化され、 これらの各機能は、NgModelController#$setValidityを通して、 値のフォーマットの機会と、フォームの入力要素の検証状態を変更する機会が与えられます。
ビューからモデルへの更新
同様にユーザーが入力要素に入力・選択をする度に、NgModelController#$setViewValueを呼び出します。 これは、NgModelController#$parsers配列内の全ての関数をプイプライン化し、 NgModelController#$parsersを通して、これらの各機能び値を変換する機会と、フォーム入力要素の検証状態を変更する機会が与えられます。

例として、下記の2つのディレクティブがあるとします。

  • まず、1つは入力されたものが正当な整数かを判定します。 例えば、1.23は少数なので不正とします。 配列へのpushの代わりに、unshiftを使用していることに注意してください。 これは、最初のparserの関数にすることで、数値変換を行う前に検証するための関数を実行し文字列の制御を行いたいためです。
  • 2つ目のディレクティブは、浮動小数点数です。 1.2と1,2の両方を、正当な浮動小数点数である1.2の数値として解析します。 ここで、1,2のような数値を不正であるとして、ユーザーの入力を許可しないブラウザでHTML5のnumberのinput型は使用できないことに注意してください。
<!doctype html>
<html ng-app="form-example1">
  <head>
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="http://code.angularjs.org/1.2.3/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        <div>
          Size (integer 0 - 10):
          <input type="number" ng-model="size" name="size"
                 min="0" max="10" integer />{{size}}<br />
          <span ng-show="form.size.$error.integer">This is not valid integer!</span>
          <span ng-show="form.size.$error.min || form.size.$error.max">
            The value must be in range 0 to 10!</span>
        </div>

        <div>
          Length (float):
          <input type="text" ng-model="length" name="length" smart-float />
          {{length}}<br />
          <span ng-show="form.length.$error.float">
            This is not a valid float number!</span>
        </div>
      </form>
    </div>
  </body>
</html>
.css-form input.ng-invalid.ng-dirty {
  background-color: #FA787E;
}

.css-form input.ng-valid.ng-dirty {
  background-color: #78FA89;
}
var app = angular.module('form-example1', []);

var INTEGER_REGEXP = /^\-?\d*$/;
app.directive('integer', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (INTEGER_REGEXP.test(viewValue)) {
          // it is valid
          ctrl.$setValidity('integer', true);
          return viewValue;
        } else {
          // it is invalid, return undefined (no model update)
          ctrl.$setValidity('integer', false);
          return undefined;
        }
      });
    }
  };
});

var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (FLOAT_REGEXP.test(viewValue)) {
          ctrl.$setValidity('float', true);
          return parseFloat(viewValue.replace(',', '.'));
        } else {
          ctrl.$setValidity('float', false);
          return undefined;
        }
      });
    }
  };
});

function Controller($scope) {
  $scope.master= {};

  $scope.update = function(user) {
    $scope.master= angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}

<!doctype html>
<html ng-app="form-example1">
  <head>
<style type="text/css">.css-form input.ng-invalid.ng-dirty {
  background-color: #FA787E;
}

.css-form input.ng-valid.ng-dirty {
  background-color: #78FA89;
}
</style>
    <script src="http://code.angularjs.org/1.2.3/angular.min.js"></script>
<script>var app = angular.module('form-example1', []);

var INTEGER_REGEXP = /^\-?\d*$/;
app.directive('integer', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (INTEGER_REGEXP.test(viewValue)) {
          // it is valid
          ctrl.$setValidity('integer', true);
          return viewValue;
        } else {
          // it is invalid, return undefined (no model update)
          ctrl.$setValidity('integer', false);
          return undefined;
        }
      });
    }
  };
});

var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (FLOAT_REGEXP.test(viewValue)) {
          ctrl.$setValidity('float', true);
          return parseFloat(viewValue.replace(',', '.'));
        } else {
          ctrl.$setValidity('float', false);
          return undefined;
        }
      });
    }
  };
});

function Controller($scope) {
  $scope.master= {};

  $scope.update = function(user) {
    $scope.master= angular.copy(user);
  };

  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };

  $scope.reset();
}

</script>
  </head>
  <body>
    <div ng-controller="Controller">
      <form name="form" class="css-form" novalidate>
        <div>
          Size (integer 0 - 10):
          <input type="number" ng-model="size" name="size"
                 min="0" max="10" integer />{{size}}<br />
          <span ng-show="form.size.$error.integer">This is not valid integer!</span>
          <span ng-show="form.size.$error.min || form.size.$error.max">
            The value must be in range 0 to 10!</span>
        </div>

        <div>
          Length (float):
          <input type="text" ng-model="length" name="length" smart-float />
          {{length}}<br />
          <span ng-show="form.length.$error.float">
            This is not a valid float number!</span>
        </div>
      </form>
    </div>
  </body>
</html>

カスタムフォーム制御の実装(ngModel使用)

Angularは、ほとんどのケースで要件を十分に満たすべき基本的なHTMLフォームの入力要素 (inputselecttextarea) の機能を実装しています。 それでも、より柔軟に使用する必要がある場合は、ディレクティブとして自身のカスタム入力要素を書くことが可能です。

カスタム入力要素がngModelと連携し、双方向のデータバインディングを実装するために必要なことは下記の通りです。

  • それをNgModelController#$formattersに渡した後に、データの描画を請け負う$renderメソッドを実装し、
  • ユーザーが入力要素とモデルを更新する必要がある度に、$setViewValueメソッドを呼び出します。 これは通常DOMイベントリスナー内で実行されます。

詳細は、$compileProvider.directiveを確認してください。

下記の例で、contentEditable要素に双方向のデータバインディングを追加する方法を確認してください。

<!doctype html>
<html ng-app="form-example2">
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div contentEditable="true" ng-model="content" title="Click to edit">Some</div>
    <pre>model = {{content}}</pre>

    <style type="text/css">
      div[contentEditable] {
        cursor: pointer;
        background-color: #D0D0D0;
      }
    </style>
  </body>
</html>
angular.module('form-example2', []).directive('contenteditable', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      // view -> model
      elm.on('blur', function() {
        scope.$apply(function() {
          ctrl.$setViewValue(elm.html());
        });
      });

      // model -> view
      ctrl.$render = function() {
        elm.html(ctrl.$viewValue);
      };

      // load init value from DOM
      ctrl.$setViewValue(elm.html());
    }
  };
});
<!doctype html>
<html ng-app="form-example2">
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
<script>angular.module('form-example2', []).directive('contenteditable', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      // view -> model
      elm.on('blur', function() {
        scope.$apply(function() {
          ctrl.$setViewValue(elm.html());
        });
      });

      // model -> view
      ctrl.$render = function() {
        elm.html(ctrl.$viewValue);
      };

      // load init value from DOM
      ctrl.$setViewValue(elm.html());
    }
  };
});
</script>
  </head>
  <body>
    <div contentEditable="true" ng-model="content" title="Click to edit">Some</div>
    <pre>model = {{content}}</pre>

    <style type="text/css">
      div[contentEditable] {
        cursor: pointer;
        background-color: #D0D0D0;
      }
    </style>
  </body>
</html>

 Back to top

© 2017 Google
Licensed under the Creative Commons Attribution License 3.0.

このページは、ページトップのリンク先のAngularJS公式ドキュメント内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • AngularJSの更新頻度が高いため、元のコンテンツと比べてドキュメントの情報が古くなっている可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。