Open4
DataTablesで作った一覧編集画面に、SpringBootでのバリデーションチェックのエラー表示をする
やりたいこと
- 画面のフォームは、DataTablesで生成した一覧編集画面。
- Springのバリデーションの仕組みを生かして、フォームの単項目チェックを行いたい。
- 単項目チェックでエラーになった場合、エラーの入力項目をハイライトしたい。また、エラーメッセージをわかりやすく出したい。
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参照。
クライアントでのエラー表示の制御
①エラー表示の初期化
一度エラーになった後、入力を訂正した場合を考慮し、リセットする。
②エラー表示項目とメッセージの制御
- 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("失敗");
});
});
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;
}
// 省略
}
- リクエストパラメータをバインドしたオブジェクトとBindingResultを、バリデーション用のメソッド(口述)に渡す。
- エラーがあったら、
- エラー内容格納用の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.
}
}
- 前提:TableDatatablesRecordのクラスでは、フィールドにバリデーション用のアノテーションをつけている。
- バリデーション自体は、SmartValidatorに任せる。
- 最終的にエラー内容は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;
}
- 「list[レコードのキー].カラム名」から、「レコードのキー」と「カラム名」を取り出す正規表現。
- 1.で取り出した内容を、FieldErrorDtoにセットしなおす。