Open4

DataTablesで作った一覧編集画面に、SpringBootでのバリデーションチェックのエラー表示をする

mIO1000mIO1000

やりたいこと

  • 画面のフォームは、DataTablesで生成した一覧編集画面。
  • Springのバリデーションの仕組みを生かして、フォームの単項目チェックを行いたい。
  • 単項目チェックでエラーになった場合、エラーの入力項目をハイライトしたい。また、エラーメッセージをわかりやすく出したい。
mIO1000mIO1000

DataTablesで入力項目ありの一覧画面を生成する

  • DataTablesの初期化で渡すcolumnオプションで、各列のrenderに、inputタグのhtmlをべた書きする。
  • createdRowオプションで、各行のtdタグのclass属性に"error-tooltip"を追加するコールバックを記載。このclass属性があることで、エラーメッセージの表示を制御する。
table_datatables.js
$(function() {

	let initialized = false;

	// 検索ボタン押下
	$("#searchB").on("click", function() {

		let _form = new FormData($("#form").get(0));
		$.ajax({
			type: "POST",
			data: _form,
			url: "/webappSample/table_datatables/search",
			processData: false,
			contentType: false,
			dataType: "json"

		}).done(function(data, textStatus, jqXHR) {
			console.log(_form);
			if (initialized) {
				// 繰り返し検索した場合、同一idで再初期化できないため、先にDataTablesを破壊し、かつDOMを削除する。
				$("#mytable").DataTable().destroy();
				$("#mytable > tr").empty();
			}

			let list = data.data;

			// DataTablesの初期化
			$("#mytable").DataTable({
				data: list,
				searching: false,
				lengthChange: false,
				pageLength: 8,
				createdRow: function ( row, data, dataIndex, cells ) {
					$(row).find("td").addClass("error-tooltip");
				},
				columns: [
					{
						title: "選択",
						data: null,
						//orderable: false,
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="checkbox" name="check">';
						},
					},
					{
						title: "No.",
						data: "no",
						visible: true,
					},
					{
						title: "社員ID",
						data: "employeeId",
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="text" name="employeeId" value="' + data + '" readonly>'
							+ '<input type="hidden" name="newLine" value="' + row.newLine + '">'
							+ '<input type="hidden" name="updateDate" value="' + row.updateDate + '">';
						},
					},
					{
						title: "社員名",
						data: "name",
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="text" name="name" value="' + data + '">';
						},
					},
					{
						title: "性別",
						data: "gender",
						visible: true,
						render: function (data, type, row, meta) {
							return '<select name="gender">' +
									'<option value="M" '+ (data === "M" ? 'selected' : '')+ '>男</option>' +
									'<option value="F" '+ (data === "F" ? 'selected' : '')+ '>女</option>' +
								'</select>';
						},
					},
					{
						title: "生年月日",
						data: "birthday",
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="date" name="birthday" value="' + data + '">';
						},
					},
					{
						title: "入社年月日",
						data: "enteringDate",
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="date" name="enteringDate" value="' + data + '">';
						},
					},
					{
						title: "退社年月日",
						data: "retirementDate",
						visible: true,
						render: function (data, type, row, meta) {
							return '<input type="date" name="retirementDate" value="' + data + '">';
						},
					},
					{
						title: "部署",
						data: "departmentId",
						visible: true,
						render: function(data, type, row, meta) {
							let html = "";
							html += '<select name="departmentId">';
							html += '<option value=""></option>';
							for (let department of departmentList) {
								if (data === department.departmentId) {
									html += '<option value=' + department.departmentId + ' selected>' + department.departmentName + '</option>'
								} else {
									html += '<option value=' + department.departmentId + '>' + department.departmentName + '</option>'
								}
							}
							html += '</select>'
							return html;
						},
					},
				],
			});

			initialized = true;

		}).fail(function(jqXHR, textStatus, errorThrown) {
			alert("失敗");
		});
	});
	
	// 省略
	
});

なお、cssについてはgithub参照。
https://github.com/mIO1000-g/webappSampleSpringBoot/blob/main/webappSample/src/main/resources/static/css/table_datatables.css

mIO1000mIO1000

クライアントでのエラー表示の制御

①エラー表示の初期化

一度エラーになった後、入力を訂正した場合を考慮し、リセットする。

②エラー表示項目とメッセージの制御

  • nameが「employeeId」=マスタのPKで、レスポンスデータのキーと一致する行の場合に、
  • レスポンスデータに格納されている列名から、エラー列を探し、
  • 対象のinput要素のclass属性に「has-error」を追加
  • さらにその要素の後ろに、エラーメッセージを設定するspan要素を追加
    ということをやっています。
table_datatables.js
	// 確定(JSON->List)ボタン押下
	$("#confirmB_JSON2List").on("click", function() {

		// リクエスト用配列
		let _data = [];
		
		// 省略

		// エラー表示を初期化						// ①
		$("table span").remove();
		$(".has-error").removeClass("has-error");

		// 送信
		$.ajax({
			type: "POST",
			data: JSON.stringify(_data),
			url: "/webappSample/table_datatables/confirm_json2list",
			contentType: "application/json",
			dataType: "json"

		}).done(function(data, textStatus, jqXHR) {

			if ("NG" === data.result) {
				// 処理結果NGの場合

				for (i = 0; i < data.data.length; i++) {
					// 返却データを走査
					$("#mytable tbody tr").each(function(index, row) {
						// テーブルの行を走査
						if ($(row).find("input[name=employeeId]input[value=" + data.data[i].key + "]").length > 0) {	// ②
							// キー(社員ID)が一致する行の場合、対象列の要素を赤くし、単項目チェックエラー内容を付加
							let element = $(row).find("input[name=" + data.data[i].column + "]");
							element.attr("class", "has-error");
							element.after('<span class="error-tooltip-text">' + data.data[i].errorMessage + '</span>');
						}
					});	
				}
			}

			// モーダル表示
			showModal(data.message);

		}).fail(function(jqXHR, textStatus, errorThrown) {
			alert("失敗");
		});
	});

mIO1000mIO1000

Springのバリデーションを利用して単項目チェックを行う

TableDatatablesController.java
@Controller
@RequestMapping("/table_datatables")
public class TableDatatablesController {

	private static final Logger logger = LoggerFactory.getLogger(TableDatatablesController.class);

	@Autowired
	private SmartValidator smartValidator;

	@RequestMapping(path = "/confirm_json2list", method = { RequestMethod.POST })
	@ResponseBody
	public ResponseDto confirmJson2List(@RequestBody List<TableDatatablesRecord> list, BindingResult br) {

		// 単項目チェック
		validate(list, br);		// 1.

		if (br.hasErrors()) {		// 2.
			// エラーがある場合
			List<FieldErrorDto> fieldErrors = getFieldErrors(br);		// 3.
			ResponseDto rd = new ResponseDto(message.getMessage("WCOM00002", null), "NG", fieldErrors);
			return rd;
		}
		
		sv.confirm(list);

		ResponseDto rd = new ResponseDto(message.getMessage("WCOM00001", null), "OK", null);

		return rd;
	}

	// 省略

}
  1. リクエストパラメータをバインドしたオブジェクトとBindingResultを、バリデーション用のメソッド(口述)に渡す。
  2. エラーがあったら、
  3. エラー内容格納用のDTOにセットする。
TableDatatablesController.java

	private void validate(List<TableDatatablesRecord> list, BindingResult br) {
		// NOTE:リクエストハンドラの引数にアノテーションをつけてValidationも可能だが、
		// 自動でBindingResultに格納されるパスが、結果をクライアントに戻したときに結局使いづらいため、自前で制御する。
		
		for (int i = 0; i < list.size(); i++) {
			TableDatatablesRecord record = list.get(i);
			// 選択した行のみ単項目チェック対象とする
			// エラー対象のフィールドを設定するために、一時的にパスをプッシュ
			br.pushNestedPath("list[" + record.getEmployeeId() + "]");			// 2.
			// SmartValidatorに、検証対象のオブジェクトを渡す
			smartValidator.validate(record, br);							// 1.
			// 追加したパスをリセット
			br.popNestedPath();										// 2.
		}
	}

  1. 前提:TableDatatablesRecordのクラスでは、フィールドにバリデーション用のアノテーションをつけている。
  2. バリデーション自体は、SmartValidatorに任せる。
  3. 最終的にエラー内容はBindingResultに格納される。BindingResult内でエラー内容を保持するときのキーとして「NestedPath」があるが、最終的に、 list[レコードのキー].カラム名 になるようにパスを構築する。
TableDatatablesController.java

	private List<FieldErrorDto> getFieldErrors(BindingResult br) {
		// BindingResultのパスから、キー情報とフィールドを取得する正規表現
		String regex = "\\[(.+?)\\]\\.(.+?)$";     // 1.
		Pattern p = Pattern.compile(regex);
		
		List<FieldErrorDto> list = new ArrayList<>();

		// BindingResultからエラーメッセージを全て取得
		for (FieldError fe : br.getFieldErrors()) {
			FieldErrorDto dto = new FieldErrorDto();
			Matcher m = p.matcher(fe.getField());
			if (m.find()) {
				// 単項目チェックエラー格納用DTOに詰めなおす     // 2.
				dto.setKey(m.group(1));
				dto.setColumn(m.group(2));
				dto.setErrorMessage(fe.getDefaultMessage());
				list.add(dto);
			}
		}

		return list;
	}

FieldErrorDto.java
@Data
public class FieldErrorDto {

	private String key;
	private String column;
	private String errorMessage;
}
  1. 「list[レコードのキー].カラム名」から、「レコードのキー」と「カラム名」を取り出す正規表現。
  2. 1.で取り出した内容を、FieldErrorDtoにセットしなおす。