カジュアルな技術ノート

小難しい技術のお話をできるだけわかりやすく...

th:field と th:object によるフォームバインディング機能(inputタグ・password編)

th:fieldth:object が提供する機能説明の続きです。
今回は input タグの type="password" をみていきましょう。

th:fieldth:object の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら

casual-tech-note.hatenablog.com

inputタグ・type="password" における提供機能

パスワード入力を受け付けるinputタグにおける th:fieldth:object の提供機能は以下の三つです。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性空文字 を代入

inputタグ・基本入力系での提供機能と似ていますが、微妙に違いますね。

value 属性 にはフィールドの値は代入されずに必ず空文字が入ります。このような仕様になっている背景をサンプルコードを通して見ていきましょう。

シンプルなサンプルコード

inputタグ・password における th:fieldth:object の提供機能を確認するために次のようなサンプル画面を考えていきましょう。 f:id:shin-kinoshita:20180923193836p:plain

名前とパスワードを入力として受け付け、受け取った値を出力するだけのシンプル画面です。
th:fieldth:object を使用した実際のコードは次の通りです。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${passwordForm.name}"></span></p>
<p>Your password is 「<span th:text="${passwordForm.password}"></span></p>

名前とパスワードの入力値を受け取るためのフォームオブジェクトとして passwordForm を用意しています。パスワード入力のレンダリング結果を見ると、id 属性name 属性th:field で指定したフィールド名、password が代入されていることが確認できます。
f:id:shin-kinoshita:20181007005620p:plain

一方 value 属性 は空になっています( 仮に password フィールドに値が入っていたとして空になります)。

完全なフロントエンドのコードはこちらに用意しておきました。

パスワードのバリデーションチェック

今回の Inputタグの type="password" では、なぜ value 属性 に空文字を代入するのでしょうか?。実はこれによって パスワードのバリデーションチェックに引っかかった際に、フィールド値を空にする手間を省いています。

ここでは例として、 パスワードの入力値は8〜16文字でなければならない という制限をかけることを考えて見ましょう。つまり入力のパスワード長がこの範囲外なら、バリデーションエラーにより再度入力画面に戻されるという仕様をイメージしてください。
f:id:shin-kinoshita:20180923195007p:plain

バリデーションを施している実装を見て見ましょう。
こちらはバックエンドにて実装します(完全版はこちら)。

public class PasswordForm {
    private String name;

    // ここでバリデーションを定義
    @Size(min = 8, max = 16)
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

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

フォームクラスの password フィールドに @Size を使用することで、文字数制限を実現しています。

そしてバリデーションエラーの有無によって遷移先を変更するする必要があります。これはコントローラーの output メソッドで設定します。

@Controller
@RequestMapping("/binding/input/password")
public class PasswordController {
    @GetMapping
    public String input(Model model) {
        model.addAttribute("passwordForm", new PasswordForm());
        return "binding/input/password/input";
    }

    @PostMapping
    public String output(@Validated  PasswordForm passwordForm, BindingResult br) {
        if (br.hasErrors()) {
            // Input Page へ遷移
            return "binding/input/password/input";
        }
        // Output Page へ遷移
        return "binding/input/password/output";
    }
}

バリデーションエラーが存在するときは Input Page へ、存在しないときは Output Page へ遷移するように分岐しています。

またエラーメッセージを表示するために、Input Page の view の実装も修正します。

<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <!-- バリデーションエラーはここで表示だよー -->
  <ul th:if="${#fields.hasErrors('password')}">
    <li th:each="e : ${#fields.detailedErrors('password')}" th:text="${e.message}" style="color: red;"></li>
  </ul>
  <!------------------------->
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

さて、今回のようにバリデーションエラーによってユーザに入力値の修正を求める場合、すでに入力してもらった全項目の値を再入力する手間を防ぐためにそのまま保持する必要があります(今回の例では name の入力値ですね)。具体的には passwordForm を output メソッドの引数で受け取った時点で model に格納されているので、これによって保持しています。

しかしパスワードは中でもデリケートな項目であるため、パスワードの入力値のバリデーション結果に関係なく再入力を促すのが普通です。つまり password フィールドだけ値を初期化したい。こういう事情で、value 属性だけには空文字を設定してくれているというわけです。

まとめ

input タグ・type="password" における th:fieldth:object における提供機能は、value 属性における扱い以外は基本入力系と同じでいわばマイナーチェンジです。
なかなか使っていても見落としがちな機能ですが意外と仕事をしている、そんな黒子役のパスワード入力におけるお話でした。

th:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)

Spring + Thymeleaf でよく見かける th:fieldth:object
フォームで入力を受け取る際に、ほぼ必ずと言っていいほどセットで使用します。

しかし入門者にとっては、とりあえず書いておけばフォームで値を受け取ることができる構文に見えかねない気もします(少なくとも、自分が最初にお目にかかった時はそんなイメージでした。。。)。

とりあえずルールとして th:object にフォームオブジェクトを、 th:field に対応するフォームオブジェクトのフィールドを書けばいいのはなんとなくわかります。
でもそれ以降の仕組みが一目ではわからず気持ち悪い。。
そんな風に感じていたので、 th:fieldth:object が実際にはどんな機能を提供しているのか、主な機能を解説します。

th:field と th:object がサポートしているタグ一覧

th:fieldth:object はどのタグにでも使用できるわけではありません。
現在デフォルトでサポートされているのは次の4つです。

  • input タグ
  • select タグ
  • option タグ
  • textarea タグ

そして タグによって th:fieldth:object が提供している機能はいくつか異なります。 また input タグに関しては type 属性によっても提供される機能が変わってきます (input タグに関しては type 属性によってはサポートされていないものもあります)。

一方で input タグでサポートしている type 属性における提供機能は大半が同じ だったりします。 今回はこの大半の type 属性群を 基本入力系 と呼ぶこととしましょう。

まずどの type 属性の input タグがこの基本入力系に分類されるのか、以下にまとめてみました(Thymeleaf 3.0現在)。

input タグの type 属性による th:field と th:object の提供機能分類
パターン type 属性一覧
基本入力系 text, hidden, detetime, datetime-local, date, time, month, week, number, range, email, url, search, tel, color
その他 password, checkbox, radio, file

上記の表に含まれない type 属性はサポート外です。
基本入力系のへの提供機能は他のタグとその他に分類されているものと共通している点も多いので、以降こちらを取り上げていきます(その他に分類されるものにも重要なものが多いのでまたの機会に。。)。

基本入力系以外における提供機能は、別記事で紹介します。

サンプルコード

実際にフォームで入力を受け取るための UI にはいろんなものがありますが、最も基本的なものの一つがテキスト入力です。 今回はテキスト入力を受け取り、その内容を出力する単純なプログラムを通して th:fieldth:object の提供機能を見ていきましょう。
つまり基本入力系の中でも input タグの type 属性が text のものを扱っていきます。

具体的には次のような入力画面と出力画面を作ることを考えてみます。

f:id:shin-kinoshita:20180904014409p:plain

例として Input Page にて name に「subaru」、text に「I love india curry」と入力し submit すると Output Page はこのようになります。

これ以上ないほどシンプルな実装です。
以下に実際のコードを書いていきましょう。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${textForm.name}"></span></p>
<p>Your text is 「<span th:text="${textForm.text}"></span></p>

ここで登場している textForm はコントローラで model に追加した入力値を受け取るためのオブジェクトです。 一応実際のバックエンドフロントエンドの完全版実装も準備して見ました(見るにしてもあまりに単純かもしれませんが。。。)。

th:field と th:object による提供機能

公式のチュートリアルhttps://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#inputs)によると、th:field は 次のような機能を提供しているとのことです(厳密にはこれ以外にもありますが、本質から外れるので割愛)。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性th:field に指定した変数に格納された値を代入

このことから Input Page の form タグは以下と概ね同じで、書き換えてあげることが可能です。

<!-- Input Page -->
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" id="name" name="name" th:value="*{name}"/>
  <p>text</p>
  <input type="text" id="text" name="text" th:value="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

元々の Input Page のレンダリングを確認しても、一致することが確認できます。
f:id:shin-kinoshita:20181010223803p:plain

これが th:fieldth:object による基本的な提供機能となります。
他の多くのタグや input の属性値でも 共通しています。

th:field と name, id, value 属性の併用

th:fieldth:object によって id 属性name 属性value 属性 が生成されることはわかりました。 ここで一つ疑問に思うこととして th:field と一緒にこれらの属性を併用して記述した場合はどうなるのでしょうか? どちらかの値が上書きされることは予想できますが、実はここの優先度は属性値によって異なります。 検証用に、以下のような view を用意してみました。

<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}" name="manualName" id="manualId" value="manualValue"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

レンダリングの結果は次のようになります。
f:id:shin-kinoshita:20181010223503p:plain
この結果からそれぞれの属性についてみてみると、

  • name 属性
    値が name であることから、th:field 属性が優先。
  • id 属性
    値が manualId であることから、id 属性が優先。
  • value 属性
    値が 空の入力値 になっていることから、th:field が優先。

実は id 属性だけ優先度が違うのです。
実際フォームバインディングに使用されるのは name 属性value 属性 のみです(詳しくは後述します)。 id 属性は通常のセレクタとして使用されることを想定しているため、このような優先度になっているものと推測されます。

まとめ

今回は th:fieldth:object を使用するケースの中でも基本的なものを紹介してみました。 th:fieldth:object は構文的に使用しがちということを言いましたが、実際の挙動はタグや属性値によって異なっています。 しっかり挙動を押さえることは、短く可読性よく記述することにも繋がるかと思うので参考になればと思います。