入力エラー時にテキストボックスの枠を赤くしたい

はじめに
---

入力エラー時にテキストボックスの枠を赤くしようとしたのですが、実装に苦労したので投稿します。

環境
---
OS: macOS Catalina 10.15.6
JDK:14.0.1
Spring Boot 2.3.3
jquery 3.3.1-1
bootstrap 4.2.1

①入力チェックにあたって
---
入力チェック(バリデーション）を行うにあたって、作者は下記のようにクラスを作成しました。

■コントローラークラス

```java:SignupController.java
package com.example.demo.login.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.login.domain.model.GroupOrder;
import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

//ユーザー新規登録用コントローラー
@Controller
public class SignupController {

	@Autowired
	private UserService userService;

	//ユーザー登録画面へ遷移
	@PostMapping("/signup")
	public String postSignUp(@ModelAttribute SignupForm form, Model model) {

		return "signup/signup";
	}

	//ユーザー新規登録実行用メソッド
	@PostMapping("/signupUser") //「GroupOrder」で設定した順序でバリデーションを実行
	public String postSignUp(@ModelAttribute @Validated(GroupOrder.class) SignupForm form, BindingResult bindingResult, Model model) {

		//バインディングでエラー発生(バリデーションエラー含む)した場合、ユーザー登録画面へ遷移
		if (bindingResult.hasErrors()) {
			return postSignUp(form, model);

		}

		//新規登録した内容をコンソールへ表示
		System.out.println(form);

		//Userインスタンスの作成
		User user = new User();

		//登録フォームに入力した内容をUserクラスにセット
		user.setUserId(form.getUserId());
		user.setMailAddress(form.getMailAddress());
		user.setPassword(form.getPassword());

		//フォーム入力内容をセットした「User」を引数にサービスクラスへ処理を投げる
		boolean result = userService.insertOne(user);

		//登録作業の実行結果をコンソールに表示
		if (result == true) {
			System.out.println("insert成功");
		} else {
			System.out.println("insert失敗");
		}

		//ログイン画面へ遷移
		return "redirect:/login";

	}

}
```

■バリデーショングループ

```java:GroupOrder.java
package com.example.demo.login.domain.model;

import javax.validation.GroupSequence;

//ユーザー新規登録とユーザー登録情報更新の際に「ValidGroup1」「ValidGroup2」の順番にバリデーションを実行
@GroupSequence({ValidGroup1.class, ValidGroup2.class})
public interface GroupOrder {


}
```

```java:ValidGroup1.java
package com.example.demo.login.domain.model;

//ValidGroup1のインターフェース
public interface ValidGroup1 {

}
```

```java:ValidGroup2.java
package com.example.demo.login.domain.model;

//ValidGroup2のインターフェース
public interface ValidGroup2 {

}
```

■入力フォームのクラス

```java:SignupForm.java

package com.example.demo.login.domain.model;


import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

import org.hibernate.validator.constraints.Length;

import lombok.Data;


//ユーザー新規登録フォームで使用するSignupForm
@Data
public class SignupForm {

	/*
	 * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類)
	 * 3字以上20字以内(ValidGroup2に分類)
	 */
	@NotBlank(groups = ValidGroup1.class)
	@Length(min = 3, max = 20, groups = ValidGroup2.class)
	private String userId;

	/*
	 * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類)
	 * メールアドレスの形式でないとエラー発生(ValidGroup2に分類)
	 */
	@NotBlank(groups = ValidGroup1.class)
	@Email(groups = ValidGroup2.class)
	private String mailAddress;

	/*
	 * null 半角スペース 空文字の場合エラー発生(ValidGroup1に分類)
	 *  3字以上20字以内(ValidGroup2に分類)
	 *  英数字のみ(ValidGroup2に分類)
	 */
	@NotBlank(groups = ValidGroup1.class)
	@Length(min = 3, max = 20, groups = ValidGroup2.class)
	@Pattern(regexp = "^[a-zA-Z0-9]+$", groups = ValidGroup2.class)
	private String password;

}
```

■エラーメッセージ 

```properties:messages.properties
#バリデーションエラーメッセージ
signupForm.userId=ユーザーID
NotBlank.signupForm.userId={0}を入力してください
Length.signupForm.userId={0}は、{2}字以上{１}字以下で入力してください

signupForm.mailAddress=メールアドレス
NotBlank.signupForm.mailAddress={0}を入力してください
Email.signupForm.mailAddress=メールアドレス形式で入力してください

signupForm.password=パスワード
NotBlank.signupForm.password={0}を入力してください
Length.signupForm.password={0}は、{2}字以上{１}字以下で入力してください
Pattern.signupForm.password={0}は半角英数字で入力してください

searchForm.keyword=キーワード
NotBlank.searchForm.keyword={0}を入力してください

#ログインエラーメッセージをカスタマイズ
AbstractUserDetailsAuthenticationProvider.badCredentials=ログインIDまたはパスワードが間違っています。
```

②入力フォームの作成（修正前)
---

当初、下記のように入力フォームのhtmlを作成しました。

```html:signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/template}">
<head>
	<meta charset="UTF-8"></meta>
    <title>SignUp</title>

</head>
<!-- ユーザー登録画面 -->
<body>
    <div layout:fragment="content">
		<div class="col-sm-4 offset-sm-4">
			<div class="page-header">
				<h1>ユーザー登録画面</h1>
			</div>
			<form method="post" th:action="@{/signupUser}" th:object="${signupForm}">

				<table class="table table-bordered table-hover">
				<tr>
					<td> <!-- modelオブジェクト「signupForm」の「userId」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする
-->
						<div class="form-group" th:classappend="${#fields.hasErrors('userId')}?'has-error'">
							<input type="text" class="form-control" placeholder="ログインID" th:field="*{userId}"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('userId')}"
								th:errors="*{userId}">
								userId error
							</span>
						</div>
					</td>
				<tr>
				<tr>
					<td>　<!-- modelオブジェクト「signupForm」の「mailAddress」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする
-->
						<div class="form-group"
							th:classappend="${#fields.hasErrors('mailAddress')}?'has-error'">
							<input type="text" class="form-control" placeholder="メールアドレス" th:field="*{mailAddress}"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('mailAddress')}"
								th:errors="*{mailAddress}">
								mailAddress error
							</span>
						</div>
					</td>
				<tr>
				<tr>
					<td>　<!-- modelオブジェクト「signupForm」の「password」フィールドでエラーが発生したら、class属性に「has-error」を追加し、テキストボックスの周りを赤くする
-->
						<div class="form-group"
							th:classappend="${#fields.hasErrors('password')}?'has-errors'">
							<input type="text" class="form-control" placeholder="パスワード" th:field="*{password}"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('password')}"
								th:errors="*{password}">
								password error
							</span>
						</div>
					</td>
				<tr>
				</table>
				<button class="btn btn-primary col-sm-6" type="submit">新規登録</button>
			</form>
		</div>
	</div>
</body>
</html>
```

html内のコメントでも記述しているように、各フィールドでエラーが発生したら、テキストボックスの枠が赤くなるよう記述したのですが、結果は下の通り枠は赤くはなりませんでした。


ちなみに「has-error」はBootstrapのclassでテキストボックスの枠を赤くしてくれるものです。

![スクリーンショット 2020-10-21 13.00.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639462/5c8e5aff-db02-d3fd-e7ab-b16e8651817f.png)

「Bootstrap４移行ガイド」を確認すると下記のような記述がありました。

**【Bootstrap3.xとの変更箇所】
各検証状態の表示スタイルからHTML5フォーム検証機能を使用したスタイルに変更
.has-warning, .has-error, .has-success, .has-feedback, .form-control-feedback は廃止
フォールバックとして、.is-invalid クラスと .is-valid クラスをサーバー側の検証用の疑似クラスの代わりに使用可能。.was-validated 親クラスは必要ない。**

URL：https://bootstrap-guide.com/components/forms?

Bootstrap４以降「has-error」は廃止、代わりに「is-invalid」が機能として提供されているようです。


③入力フォームの修正
---
divタグ内ではなく、inputタグの中に記述します。

```html:signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/template}">
<head>
	<meta charset="UTF-8"></meta>
    <title>SignUp</title>

</head>
<!-- ユーザー登録画面 -->
<body>
    <div layout:fragment="content">
		<div class="col-sm-4 offset-sm-4">
			<div class="page-header">
				<h1>ユーザー登録画面</h1>
			</div>
			<form method="post" th:action="@{/signupUser}" th:object="${signupForm}">

				<table class="table table-bordered table-hover">
				<tr>
					<td>
						<div class="form-group">
							<!-- modelオブジェクト「signupForm」の「userId」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする-->
							<input type="text" class="form-control" placeholder="ログインID" th:field="*{userId}"
							th:classappend="${#fields.hasErrors('userId')}?'is-invalid'"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('userId')}"
								th:errors="*{userId}">
								userId error
							</span>
						</div>
					</td>
				<tr>
				<tr>
					<td>
						<div class="form-group">
							<!-- modelオブジェクト「signupForm」の「mailAddress」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする-->
							<input type="text" class="form-control" placeholder="メールアドレス" th:field="*{mailAddress}"
							th:classappend="${#fields.hasErrors('mailAddress')}?'is-invalid'"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('mailAddress')}"
								th:errors="*{mailAddress}">
								mailAddress error
							</span>
						</div>
					</td>
				<tr>
				<tr>
					<td>
						<div class="form-group">
							<!-- modelオブジェクト「signupForm」の「password」フィールドでエラーが発生したら、class属性を追加し「is-invalid」でテキストボックスの周りを赤くする-->
							<input type="text" class="form-control" placeholder="パスワード" th:field="*{password}"
							th:classappend="${#fields.hasErrors('password')}?'is-invalid'"/>
							<span class="text-danger"
								th:if="${#fields.hasErrors('password')}"
								th:errors="*{password}">
								password error
							</span>
						</div>
					</td>
				<tr>
				</table>
				<button class="btn btn-primary col-sm-6" type="submit">新規登録</button>
			</form>
		</div>
	</div>
</body>
</html>
```

実行すると下のように、入力エラー時テキストボックスの枠を赤くすることができました。

![スクリーンショット 2020-10-21 13.28.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639462/fb513c89-50c0-9c11-f507-aa344e7983da.png)

参考:
https://www.e-pokke.com/blog/bootstrap4-invalid-feedback.html
https://learning-collection.com/thymeleaf%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/