スポンサーリンク

【SpringBoot+Thymeleaf】動的Tableデータをサーバへ送信

Javascriptで動的に作成したtableのtbodyデータを、Thymeleafを使用してサーバへ送信する方法です。
仕事で初めてSpringBoot + Thymeleaf + Mybatisを扱って、この送信方法に2日間以上悩んだのと、ネットで調べたとおりに実装してもうまく動かなかったので、備忘録として残しておきます。

スポンサーリンク

前提

環境構築、th:fieldの使い方などは知っているものとして記載しています。

単にソースコードを書いてもわかりづらいので、以下の目的に沿って書いてます。

  • table用データとして本の題名/金額を、ブラウザ上でデフォルト表示
  • 動的に追加された題名/金額を、form postにてサーバへ送信

尚、ソースコードには必要最低限しか書いてません。

Formクラスについて

サーバとブラウザ間でのやりとりのためのFormクラスを以下とします。

BookPageForm.java

public class BookPageForm {
  ArrayList<BookInfo> bookInfoList = new ArrayList<>(); // 本の情報(題名/金額)リストを格納
}

BookInfo.java

public class BookInfo {
  private String title;  // 題名
  private Integer price; // 金額
  // getter/setterは省略

  // 必要に応じて、コンストラクタを追加
}

ここで重要なのは、BookInfoクラスをBookPageFormクラス内で宣言しないことです。
仮に以下のようにすると、コンパイルは通りますが、<init>に失敗しましたといったExceptionが発生し、実行時うまく動きません。

// ダメな例
public class BookPageForm {
  ArrayList<BookInfo> bookInfoList = new ArrayList<>(); // 本の情報(題名/金額)リストを格納

  public class BookInfo {
    private String title;  // 題名
    private Integer price; // 金額
    // getter/setterは省略

    // コンストラクタを宣言してもダメ
  }
}

ちなみに私はこれが原因とわかるまで1日以上潰しました。。。

Thymeleafについて(HTML展開後のコードあり)

次にHTML上のThymeleafのコードです。

<form id="form" th:action="@{/book}" method="post" th:object="${bookPageForm}">
  <table>
    <thead>
      <tr>
        <th>題名</th><th>金額</th>
      </tr>
    </thead>
    <tbody id="tbl_tbody"> <!-- idは無くても良い -->
      <tr th:each="bookInfo,stat : *{bookInfoList}" >   <!-- サーバで設定されたデータを表示 -->
        <!-- 以下は表示用 -->
        <td th:text="${bookInfo.title}"></td>
        <td th:text="${bookInfo.price}"></td>
        <!-- 以下はpost送信用 -->
        <td style='display:none;'>
          <input type="hidden" th:field="*{bookInfoList[__${stat.index}__].title}" />
          <input type="hidden" th:field="*{bookInfoList[__${stat.index}__].price}" />
        </td>
      </tr>
    </tbody>
  </table>
</form>

表示用とpost送信用で分けてるのは仕事上の都合です。
td内にタグなどあると、Javascriptで操作する際に都合が悪かったので。実際に分けるかどうかは都合に合わせてください。

ここでのポイントとして、th:each内のstatです。
「__${stat.index}__」で何番目のデータかを得ることができます。サーバに送信する際、配列の形で送信するため、このstatが重要になってきます。

このstatはステータス変数と呼ばれ、th:each内の繰り返し処理のステータスを知ることができます。
変数名はstatでなくても良いです。詳細については、下記を参照してください。
(下記説明ではiterStatという変数名になっています)

Tutorial: Using Thymeleaf (ja)

上記ThymeleafのコードがHTMLに展開されると、以下のようになります。

<form id="form" action="/book" method="post">
  <table>
    <thead>
      <tr>
        <th>題名</th><th>金額</th>
      </tr>
    </thead>
    <tbody id="tbl_tbody"> <!-- idは無くても良い -->
      <tr>   <!-- サーバで設定されたデータを表示 -->
        <!-- 以下は表示用 -->
        <td>鬼滅の刃1巻</td>
        <td>450</td>
        <!-- 以下はpost送信用 -->
        <td style='display:none;'>
          <input type="hidden" id="bookInfoList0.title" name="bookInfoList[0].title" value="鬼滅の刃1巻" />
          <input type="hidden" id="bookInfoList0.price" name="bookInfoList[0].price" value="450" />
        </td>
      </tr>
      <tr>   <!-- サーバで設定されたデータを表示 -->
        <!-- 以下は表示用 -->
        <td>鬼滅の刃2巻</td>
        <td>470</td>
        <!-- 以下はpost送信用 -->
        <td style='display:none;'>
          <input type="hidden" id="bookInfoList1.title" name="bookInfoList[1].title" value="鬼滅の刃2巻" />
          <input type="hidden" id="bookInfoList1.price" name="bookInfoList[1].price" value="470" />
        </td>
      </tr>
    </tbody>
  </table>
</form>

この展開されたコードが知りたいのに検索してもなかなか出てこなかったので、ここでちゃんと(?)載せておこうと思いました。

ここでのポイントは、inputタグ内の展開されたid属性とname属性です。
idはページ内で一意になるように、またnameは配列になるように展開されています。

Javascriptを熟知している人なら、これを見れば動的に行を追加することは容易いのではないでしょうか。

Controllerクラスについて

サーバ側で受け取るController側のコードです。

BookController.java

// GET時(初期表示)
@GetMapping(value = "/book")
public ModelAndView get(
            BookPageForm formData,
            ModelAndView mav) {

  // 初期表示用
  BookInfo bookInfo = new BookInfo();
  bookInfo.setTitle("鬼滅の刃1巻");
  bookInfo.setPrice(450);
  formData.getBookInfoList.add(bookInfo);

  bookInfo = new BookInfo();
  bookInfo.setTitle("鬼滅の刃2巻");
  bookInfo.setPrice(470);
  formData.getBookInfoList.add(bookInfo);

  mav.addObject("bookPageForm", formData);
  return mav;
}

// POST時
@PostMapping(value = "/book")
public ModelAndView post(
            @ModelAttribute("bookPageForm") @Validated BookPageForm formData,
            ModelAndView mav) {

  ArrayList<BookInfo> bookInfoList = formData.getBookInfoList();
  for (BookInfo bookInfo : bookInfoList) {
    if ((bookInfo.getTitle() != null) && (bookInfo.getPrice() != null)) {  // (1)
      String title = bookInfo.getTitle();
      String price = bookInfo.getPrice();
    }
  }

  .. 以下、省略 ..
}

ここでのポイントは、(1)でチェックを行っていることです。
例えば、動的に行を追加/削除することで、以下のように配列が飛び飛びになることがあります。

// [0]、[1]、[2]を追加後、[1]を削除。([1]が無い)

.. 省略 ..
  <input type="hidden" id="bookInfoList0.title" name="bookInfoList[0].title" value="鬼滅の刃1巻" />
  <input type="hidden" id="bookInfoList0.price" name="bookInfoList[0].price" value="450" />
.. 省略 ..
  <input type="hidden" id="bookInfoList2.title" name="bookInfoList[2].title" value="鬼滅の刃3巻" />
  <input type="hidden" id="bookInfoList2.price" name="bookInfoList[2].price" value="480" />
.. 省略 ..

この場合、[1]のtitle/priceは、サーバ側ではnullになります。
もし仕様上、両方ともnullになる可能性があるのであれば、他に設定有無を設けても良いかと思います。

もし送信時にIndexOutOfBoundsExceptionが発生したら

サーバへ送信時、もし以下のようにIndexOutOfBoundsExceptionが発生した場合は、

Invalid property 'bookInfoList[257]' of bean class [com.hoge.hoge.form.BookPageForm]: Index of out of bounds in property path 'bookInfoList[257]'; nested exception is java.lang.IndexOutOfBoundsException: Index 257 out of bounds for length 256

Contollerクラスに以下を追加してください。

@InitBinder
public void initBinder(WebDataBinder binder) {
  binder.setAutoGrowCollectionLimit(1024);
}

SpringBootではFormで扱える配列数の初期値が256個となっているため、上記にてその制限を1024個へ変更しています。尚、例として1024個としていますので、必要に応じて増減させてください。

(参考)JavaScriptにて行を動的に追加する方法

タイトルにもあるので、動的に追加する方法も載せておこうかと思います。ただし、他にもいろんな方法があるので、あくまで参考程度に留めておいてください。

ここでは、id=”add-book”のボタンを押すと、題名「鬼滅の刃○巻」、金額「450」の行を追加しています。

jQuery(function($) {

  // 初期表示行数を取得(配列インデックスの初期値として使用)
  var rowIndex = $('#tbl_tbody > tr').length;

  // 追加ボタン押下処理
  $('#add-book').click(function() {

    // 行テンプレート
    var rowTemplate = `
      <tr>
        <!-- 以下は表示用 -->
        <td>{title}</td>
        <td>{price}</td>
        <!-- 以下はpost送信用 -->
        <td style='display:none;'>
          <input type="hidden" id="bookInfoList{rowIndex}.title" name="bookInfoList[{rowIndex}].title"  value="{title}" />
          <input type="hidden" id="bookInfoList{rowIndex}.price" name="bookInfoList[{rowIndex}].price"  value="{price}" />
        </td>
      </tr>
    `;

    // (1) 行テンプレートに埋め込むデータ
    var title = "鬼滅の刃" + (rowIndex+1) + "巻";
    var price = 450;

    // (2) 行テンプレートへ当てはめ
    var newRow = rowTemplate
                     .split('{title}').join(title)
                     .split('{price}').join(price);

    // (3) 行を追加
    $('#tbl_tbody').append(newRow);

    // 次に追加ボタン押された時用に、インデックス値をインクリメント
    rowIndex++;
  }

}

参考なので詳細な説明は省きますが、tableのtbodyタグにidを設定(上記ではtbl_tbody)しておき、追加ボタンを押すことで、行を追加しています。

行の追加処理では、行テンプレートrowTemplateを作成(ThymeleafからHTML展開されたコードを参考に)しておき、この行テンプレートに対して、split(x).join(y)を使用して文字列xを文字列yへ変換しています。
その後、append(newRow)により、tbody部へ<tr>〜</tr>を追加しています。

追加するコードが見易いので、私はよくこのパターンを使用してます。

最後に

いかがでしたでしょうか?

コードにするとそんなに難しいことは行ってないのですが、この方法にたどり着くまでが長い道のりでした。

同じように悩んでいる人が、ここを見てスッキリしてもらえたらと思います。

コメント