Spring+Thymeleafで動的フォーム

Thymeleafの機能で動的フォームを実装する方法について。

Thymeleafの公式チュートリアルにも記載されている、サーバ側でリスト操作して画面に再表示する方法をやってみました。

環境

  • Spring Boot: 2.3.3
  • Thymeleaf: 3.0.11

実装例

単語帳を例に以下のような画面を作成します。

「追加」ボタンを押下すると、リストの一番下に入力欄が追加されます。

入力欄の右側にある「削除」ボタンを押下すると、その行の入力欄が削除されます。

実装方法

フォームのドメインクラス(DTO)

まずドメインクラスを以下のように作成します。

@Data
public class Flashcard {
   	
   	private String title;			// 単語帳のタイトル
   	private List<Card> cardList;	// 単語リスト
}
@Data
public class Card {
   	
   	private String word;	// 単語
   	private String detail;	// 説明
}

Thymeleaf

<form method="post" th:action="@{/flashcard/edit}" th:object="${flashcard}">
	<label>タイトル</label>
	<input type="text" th:field="*{title}"><br>
	<table>
		<tr>
			<th>単語</th>
			<th>説明</th>
			<th></th>
		</tr>
		<tr th:each="card, cardStat : *{cardList}">		<!-- ① -->
			<td>
				<!-- ② -->
				<input type="text" th:field="*{cardList[__${cardStat.index}__].word}">
			</td>
			<td>
				<!-- ② -->
				<input type="text" th:field="*{cardList[__${cardStat.index}__].detail}">
			</td>
			<td>
				<!-- ③ -->
				<button type="submit" name="remove" th:value="${cardStat.index}">削除</button>
			</td>
		</tr>
	</table>
	<button type="submit" name="add">追加</button>
</form>

要点は以下の通りです。

①繰り返し処理

th:each="card, cardStat : *{cardList}"

  • card:仮変数
  • cardStat:繰り返し処理のステータス変数

②データバインド

th:field="*{cardList[__${cardStat.index}__].word}"

  • 繰り返し処理以外の時と同様にth:field属性でデータバインディングします。
  • データバインディングには仮変数は使わず、リストのインデックスを指定した形式で記述します。
  • リストのインデックスにはプリプロセッシング式(__${expression}__)を使用します。

③ボタンに値を設定する

th:value="${cardStat.index}"

  • th:value属性でサーバに送信するボタンの値を設定します。
  • ボタンの値には、ステータス変数のindexプロパティで行番号を指定します。

Spring:コントローラクラス

@Controller
public class FlashcardController {

	// ①初期表示
	@GetMapping("/flashcard")
	public String load(@ModelAttribute Flashcard flashcard, Model model) {
		
		// リストの初期化(入力欄を常に1行以上表示する)
		List<Card> list = new ArrayList<Card>();
		Card card = new Card();
		list.add(card);
		flashcard.setCardList(list);
		
		return "flashcard";
	}
	
	// ②「追加」ボタン押下
	@PostMapping(value = "/flashcard/edit", params = "add")
	public String addList(@ModelAttribute Flashcard flashcard, Model model) {
		
		// リスト最後尾に1行追加
		flashcard.addList();
		
		return "flashcard";
	}
	
	// ③「削除」ボタン押下
	@PostMapping(value = "/flashcard/edit", params = "remove")
	public String removeList(@ModelAttribute Flashcard flashcard, Model model, HttpServletRequest request) {
		
		// 行番号を指定して削除
		int index = Integer.valueOf(request.getParameter("remove"));
		flashcard.removeList(index);
		
		return "flashcard";
	}
}

要点は以下の通りです。

①初期表示

  • リストを初期化する時、最低1行は表示するようにします。
  • リストの要素数が0の場合、HTMLに変換される時に<tr>の中身が空になり、「追加」ボタンを押下した時にエラーになります。

②「追加」ボタン押下

  • ドメインクラスに作成したメソッドを呼び出して、リストに要素を一つ追加します。

③「削除」ボタン押下

  • HttpServletRequestオブジェクトからサーバに送信された値を取得できます。
  • request.getParameter("remove")で削除ボタンに設定した名前を指定して、value値を取り出します。
  • ドメインクラスに作成したメソッドを呼び出して、リストから指定した位置の要素を削除します。

Spring:ドメインクラス

リストへの追加・削除のために、以下のようにメソッドを追加します。

@Data
public class Flashcard {

	private String title;					// 単語帳のタイトル
	private List<Card> cardList;	// 単語リスト
	
	// リストに新規項目を追加
	public void addList() {
		
		Card card = new Card();
		cardList.add(card);
	}
	
	// リストから項目を削除
	public void removeList(int index) {
		
		// 入力欄は最低1行は残す
		if (cardList.size() > 1) {
			cardList.remove(index);
		}
	}
}

削除メソッド

  • リストの要素数が1以下の場合は要素を削除しないようにします。
  • 初期表示時と同様に、リストの要素数が0の場合「追加」ボタン押下時にエラーとなります。

参考