フォーム入力と検証

フォーム入力はアプリケーションにとっての血液です; フォーム入力はユーザーが重要な情報を入力するのに最も効果的な方法です。 それが検索フォームであれログイン画面であれ、あるいは複数ページにわたる登録ウイザードであれ、フォームはユーザーが自分自身をアプリケーションに対して表現する方法です。

Tapestryはフォーム生成と入力検証に優れています。入力検証は宣言的で、あなたはフィールドにどの検証を適用するかをただTapestryに指示するだけです。 そしてその検証はサーバ側で実行されるだけでなく(実装されれば)クライアント側でも同様に実行されます。

さらに、Tapestryはエラーをユーザーに示すだけでなく、エラーのあるフィールドやフィールドのラベルを(主にCSSを用いて)装飾します。

Formコンポーネント

Tapestryのフォームを支える中核は Form コンポーネントです。 Formコンポーネントは TextFieldTextAreaCheckbox のような全ての フィールドコンポーネント を取り囲みます。

Formコンポーネントは様々なコンポーネントイベントを生成します。 そして、あなたはそのイベントに対するイベントハンドラメソッドを用意することができます。

Formコンポーネントはレンダリング時にふたつの通知を発行します: 最初が "prepareForRender" で、その後が "prepare" です。 これらは、Form内で参照しているフィールドやプロパティの準備をする機会をFormのコンテナに与えます。 これは、例えばレンダリングに必要となる一時的なエンティティオブジェクトを作成したり、 編集するエンティティをデータベースから読み込んだりするのにちょうどよいでしょう。

クライアント側でユーザーがフォームをサブミットすると、サーバ側では以下の順番で処理が進みます。

Formは、最初に "prepareForSubmit" 通知を発行し、次に "prepare" 通知を発行します。 Formのコンテナはこれらの通知を受け取り、オブジェクトの初期化を行いフォームのサブミットによって送られてくる情報を受け取る準備をすることができます。

次に、フォーム内の全てのフィールドがリクエストから値を取り出し、それを検証し(有効な値であれば)フィールドの値を更新します。

Tapestry 4 ユーザーへ: Tapestry 5 では、脆い "フォーム巻き戻し" の手法は用いません。 かわりにレンダリングの際にhiddenフィールドを生成し、そこにフォームをサブミットしたときに必要な情報を格納します。

各フィールドがそれぞれ処理を終えた後、Formは "validate" イベントを発行します。 これは宣言的な検証では行えない複数のフィールドに跨がる検証を行う機会を与えます。

次に、Formは検証エラーがあったかどうか判定します。もし検証エラーがあれば、そのサブミットは不成功であり "failure" イベントを発行します。検証エラーが無ければ "success" イベントを発行します。

最後に、Formは "submit" イベントを発行します(これはサブミットの成功、失敗に関わらないロジックのためです)。

検証エラーの追跡

Formには ValidationTracker が関連付けられています。ValidationTrackerはフォーム内の各フィールドのユーザーからの入力と検証エラーを全て追跡します。 ValidationTrackerはFormのtrackerパラメータによってフォームに与えることができますが、 trackerパラメータを使う事はめったにありません。

Formは isValid() メソッドと getHasErrors() メソッドを持っています。 これらはFormのValidationTrackerがエラーを含んでいるかどうか調べるのに使用します。

あなたのロジック内でエラーを記録することもできます。 Formはふたつの異なるバージョンの recordError() メソッドを持っています。 ひとつは Field を明示するものです (Fieldはフォーム要素の全コンポーネントが実装しているインターフェースです)。 もうひとつは特定のフィールドには関連付いていない "グローバル" エラー用です。

リクエストを跨いだデータの格納

他のアクションリクエストと同様、フォームのサブミット後にリダイレクトを送りクライアントはページを再レンダリングします。 そのためValidationTrackerはリクエストを跨いで永続的に保存されなければなりません。 そうでなければ検証の情報を全て失ってしまいます(Formが提供するデフォルトのValidationTrackerは永続化されます)。

同様に、コンポーネントによって更新される各フィールドも永続化されるべきです。

例えば、ユーザー名とパスワードを入力するログインページは次のようになるでしょう:

public class Login
{
    @Persist
    private String userName;

    private String password;

    @Inject
    private UserAuthenticator authenticator;

    @Component(id = "password")
    private PasswordField passwordField;

    @Component
    private Form form;

    String onSuccess()
    {
        if (!authenticator.isValid(userName, password))
        {
            form.recordError(passwordField, "Invalid user name or password.");
            return null;
        }

        return "PostLogin";
    }

    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        password = password;
    }

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        userName = userName;
    }
}

フォームのサブミットは ふたつの リクエスト(サブミット自体とその後のページの再レンダリング)であるという事実のために、 _userName フィールドに格納されている値をふたつのリクエスト間で永続化する必要があります。 同様に _password フィールドについても永続化の必要があるかもしれませんが、 PasswordField コンポーネントは値をレンダリングしません。

onSuccess() メソッドがパブリックではない事に注目してください; イベントハンドラメソッドはプライベートも含めてどの可視性でも構いません。 同じパッケージ内のテストケースクラスからテストできるようにするために、パッケージプライベート(つまり修飾子なし)とするのが一般的です。

Formが "success" イベントを発行するのは、その前に検証エラーが無かった場合だけです。 これはイベントハンドラメソッドの最初の行に if (_form.getHasErrors()) return; を書く必要は無いという事を意味します。

最後に、ビジネスロジックがどのように検証に当てはめられているのかを見てください。 UserAuthenticatorサービスがユーザー名と(平文)パスワードの検証を行います。 UserAuthenticatorサービスがfalseを返した場合、Formコンポーネントにエラーを記録します。 その際、PasswordFieldのインスタンスを最初のパラメータとして与えています; これによりFormが再レンダリングされるときにパスワードフィールドとそのラベルが装飾され、エラーがあることをユーザーに伝えます。

フィールドとラベルの設定

次のページテンプレートはTapestryに必要な最低限のものを含んでいます:

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
    <head>
        <title>Login</title>
    </head>
    <body>
        <h1>Please Login</h1>

        <form t:id="form">

            <t:errors/>

            <t:label for="userName"/>:
            <input t:type="TextField" t:id="userName" t:validate="required,minlength=3" size="30"/>
            <br/>
            <t:label for="password"/>:
            <input t:type="PasswordField" t:id="password" t:validate="required,minlength=3" size="30"/>
            <br/>
            <input type="submit" value="Login"/>
        </form>
    </body>
</html>

TapestryのFormコンポーネントにはフォームのサブミット先URLを生成する責任があります(これはTapestryの責任であってあなたの責任ではありません)。

Errors コンポーネントはForm内に置く必要があります。 ErrorsコンポーネントはForm内の全てのフィールドの全てのエラーをひとつのリストとして出力します。また、その出力の見栄えを良くするために簡単なスタイルシートを使用しています。

TextFieldのような各フィールドコンポーネントは Label コンポーネントとペアになっています。Labelコンポーネントはフィールドに結びついた <label> 要素をレンダリングします。 これはユーザビリティの点で、特に視覚障害のあるユーザーにとってとても重要なことです。 また、ラベルのテキストをクリックすると対応するフィールドにカーソルが移動するようになります。

Labelの for パラメータはコンポーネントのidです。

TextFieldのコンポーネントidを userName としています。value パラメータを指定することもできますが、 コンテナ、つまりLoginページのプロパティにTextFieldのidと同じものがあればそれがデフォルトとなります。

大雑把に言うと、フィールドには常に固有のidを付けるべきです(このidはレンダリングされるタグの name 属性と id 属性を生成するのに用いられます)。 valueパラメータを省略できることは、テンプレートの内容がゴチャゴチャしたものになるのを防ぐのに役立つでしょう。

どの検証をフィールドに対して実行する必要があるかを validate パラメータで指定します。これは検証名のリストです。 各検証はTapestry内で構成済みですが、利用可能な検証を追加することもできます。"required" はビルトイン検証のひとつで、サブミットされた値が空文字でないことを検証します。 また、"minlen" は指定された長さ以上の値であるかどうかを検証します。

validate パラメータは t: プリフィックスを用いてTapestry名前空間に置かれています。これは厳密に必要ではなく、どちらにしてもテンプレートは well-formed です。 しかし、Tapestry固有の値をTapestry名前空間に置く事でテンプレートは valid となります。

エラーとその装飾

注意: このセクションはクライアント側の入力検証の実装に対応した更新がされていません。

最初にLoginページにアクセスすると普通にフィールドとフォームが表示され、入力待ちとなります:

フォームの初期状態

Labelコンポーネントがフィールドの名前をどのように表示しているかに注目してください。明示的に設定などしていないテキストが表示されています。 コンポーネントid("userName" と "password")が "User Name" と "Password" に変換されているのです。

そのままフォームをサブミットすると、フィールドは "required" 制約に違反しそのエラーをユーザーに示すためにページが再表示されるでしょう。

エラーとその装飾

ここには一組の巧みな処理が含まれています。ひとつは、Tapestryが すべての フィールドの すべての エラーを追跡しているということです。 それらのエラーをErrorsコンポーネントがフォームの上部に表示します。さらに、デフォルトの検証エラー装飾(default validation decorator) がラベルとフィールドのCSSクラスに "t-error" を加えることで装飾を施します。 Tapestryが提供するデフォルトのCSSスタイルシートでは "t-error" クラスによって赤く表示されます。

次に、ユーザー名を入力しパスワードには必要な長さに満たない文字列を入力してサブミットします。

最小長制限のエラーメッセージ

ユーザー名フィールドはOKで、パスワードフィールドにだけエラーがあります。PasswordFieldコンポーネントは常に空欄で表示されます。 そうしないと不完全なパスワードが表示されてしまいます。

十分な長さのパスワードを入力しサブミットすると、Loginページ内のロジックがどのようにエラーをフィールドに結びつけているか見ることができます。

アプリケーションが生成したエラー

これは快適でシームレスです; ビルトインの検証とアプリケーションロジックによって生成されたエラーの見た目や振る舞いが同じなのです。

@Validateで入力検証を一カ所にまとめる

TextField、PasswordField、TextAreaコンポーネントなどの validate パラメータの代わりに Validate アノテーションを使うことができます。 validate パラメータがバインドされていない場合は@Validateアノテーションがチェックされ、その値が入力検証の定義として使用されます。

このアノテーションはgetterメソッド、setterメソッドまたはフィールドに付けることができます。

入力検証メッセージのカスタマイズ

("required" や "minlength" などの)各入力検証はその条件に違反した時、 つまりユーザーの入力が不適切であった場合にデフォルトのメッセージが(クライアント側またはサーバ側で)用いられます。

このメッセージをカスタマイズするには、ページ(またはコンポーネント)のメッセージカタログに項目を追加します。 他のローカライズされたメッセージと同様、入力検証メッセージはアプリケーションメッセージカタログに格納することもできます。

最初に、フォームId-フィールドId-検証名-message というキーがあるかどうか調べられます。

  • フォームId: フォームコンポーネントのローカルコンポーネントid
  • フィールドId: (TextFieldなどの)フィールドのローカルコンポーネントid
  • 検証名: "required"、"minlength" といった入力検証の名前

このキーが見つからなかった場合、次に フィールドId-検証名-message というキーを調べます。

そのキーも見つからない場合は組み込みの入力検証メッセージが使用されます。

入力検証メッセージのカスタマイズ / BeanEditFormの場合

BeanEditFormコンポーネントでも入力検証メッセージのカスタマイズができます。 フォームId の箇所はBeanEditFormコンポーネントのidとなります(内部のFormコンポーネントのidではありません)。 フィールドId の箇所はプロパティ名となります。

入力検証の条件をメッセージカタログ内で設定する

validatorパラメータ(または@Validatorアノテーション)に検証条件を記述しなかった場合、 メッセージカタログ内に検証条件が記述されているものとして扱われます。

これは、検証条件をインラインで記述しにくい場合に役立ちます。たとえば、regexpバリデータの正規表現を記述するような場合です。

キーは入力検証メッセージをカスタマイズする場合と似ています: フォームId-フィールドId-検証名 または フィールドId-検証名 です。

例えば、次のようなテンプレートがあったとします:

  <t:textfield t:id="ssn" validate="required,regexp"/>

そして、メッセージカタログの内容は次のようになります:

ssn-regexp=\d{3}-\d{2}-\d{4}
ssn-regexp-message=Social security numbers are in the format 12-34-5678.

これもまた、入力検証メッセージの場合と同じようにBeanEditFormに適用することができます。 フォームIdの箇所がBeanEditFormのコンポーネントidとなり、フィールドIdの箇所がプロパティ名となります。

Translatorを上書きするためのイベント

TextField, PasswordField, TextArea コンポーネントは皆、translate パラメータを持っています。 FieldTranslator オブジェクトはサーバ側の値をクライアント側の文字列に変換するものです。

多くの場合、translate パラメータを明示的に指定することはありません; フィールドに関連付けられたプロパティの型を基にしてTapestryが適切な値を決定します。

translator を差し替えたい場合もあるでしょう。これはコンポーネント上で発生するふたつのイベント "toclient" と "parseclient" によって実現できます。

"toclient" イベントハンドラにはサーバ側の値のオブジェクトが渡されます。そして、フィールドのデフォルト値となる文字列を返します。 イベントハンドラが無い場合またはイベントハンドラがnullを返した場合は、デフォルトの Translator を使用してサーバ側の値を文字列に変換します。

例えば、quantity というフィールドがあり、初期値を0ではなく空白としたい場合:

  <t:textfield t:id="quantity" size="10"/>

  . . .

  private int quantity;

  String onToClientFromQuantity()
  {
    if (quantity == 0) return "";

    return null;
  }

とりあえずここまでは良いのですが、もしこのフィールドが必須ではなく空白のままユーザーがフォームをサブミットした場合、 空文字は整数値として不適切なので妥当性検証のエラーが発生するでしょう。

そこで、"parseclient" イベントハンドラを使用します:

  Object onParseClientFromQuantity(String input)
  {
    if ("".equals(input)) return 0;

    return null;
  }

このイベントハンドラメソッドは translator よりも優先されます。この例では空文字かどうかチェックし(そして入力値はnullかもしれません!)、 空文字の場合は0を返します。

"toclient" の場合と同じく、nullを返した場合は通常の translator が使用されます。

このイベントハンドラは、パースできない値を渡されたときに ValidationException を投げることができます。

さて、独自の妥当性検証を行い場合はどうするのでしょう? もうひとつのイベント、"validate" があります:

  void onValidateFromCount(Integer value) throws ValidationException
  {
    if (value.equals(13)) throw new ValidationException("Thirteen is an unlucky number.");
  }

このイベントハンドラは通常の妥当性検証の に実行されます。そして、パース済み の値が渡されます (クライアントから受け取った文字列ではなく、translator または "parseclient" イベントハンドラでサーバ側の値に変換されたオブジェクトです)。

このメソッドは値を返しません。しかし、妥当性検証のエラーを示すために ValidationException を投げることができます。

注意: これらのイベントは サーバ側では 排他的です。つまりある状況下では、 サーバ側では妥当と判定されるはずの入力値がクライアント側の妥当性検証で拒否されてしまうことがあるということです。 この機能を使いたい場合はクライアント側での入力検証を無効にする必要があるかもしれません。