Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その11 )( 登録画面作成 )
概要
Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その10 )( ログイン画面作成3 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 登録画面 ( 入力→確認→完了 ) の作成、確認。ただし入力画面、確認画面までです。
- 入力方法が
<input type="text" .....
だけだったので、登録画面の Continent の入力項目をテキスト入力ではなくドロップダウンリストに変更します。
ソフトウェア一覧
参考にしたサイト
Hibernate Validator - Chapter 2. Declaring and validating bean constraints
http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-bean-constraints.htmlTERASOLUNA Global Framework Development Guideline 1.0.1.RELEASE documentation - 5.5. 入力チェック
http://terasolunaorg.github.io/guideline/1.0.1.RELEASE/ja/ArchitectureInDetail/Validation.htmlTutorial: Thymeleaf + Spring - 7.5 Dropdown/List selectors
http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#dropdownlist-selectorsHOW-TO: USING @PROPERTYSOURCE ANNOTATION IN SPRING 4 WITH JAVA 7
http://blog.codeleak.pl/2013/11/how-to-propertysource-annotations-in.htmlReading a List from properties file and load with spring annotation @Value
http://stackoverflow.com/questions/12576156/reading-a-list-from-properties-file-and-load-with-spring-annotation-value- properties ファイルに記載したカンマ区切りのデータを @Value アノテーションで List 型のフィールドにセットする方法が記載されています。
m4hv-extensions Document
http://maru.sourceforge.jp/m4hv-extensions/document.html- ValidationMessages.properties の日本語メッセージを作る時に参考にしました。
メンテナブルCSS
https://www.cyberagent.co.jp/corporate/techreport/report/id=7926- js で利用する DOM の名前をどうやってつけたらよいのか
http://willnet.in/78 GitHub - Styleguide - CSS
https://github.com/styleguide/css- 上の3つは js- プレフィックスに関してのメモです。何かで Javascript で操作するものは js- を付けて区別した方がよいという記事を見た記憶があったのですが、明確に覚えていなかったのでちょっと調べました。今作っている例だと class ではなく id に js- を付けた名称を付けてしまっていますね。。。 今度は class に付けるようにします。
手順
1.0.x-makecountryinput ブランチの作成
- IntelliJ IDEA 上で 1.0.x-makecountryinput ブランチを作成します。
Form クラスの作成
- 画面からの入力データを格納する Form クラスを作成します。src/main/java/ksbysample/webapp/basic/web の下に CountryForm.java を作成します。作成後、リンク先のその1の内容 に変更します。
CountryController クラスの修正、input.html, confirm.html の修正
今回 Continent の入力項目をテキスト入力ではなくドロップダウンリストに変更します。ドロップダウンリストに表示するリストは properties ファイルに定義してそこから取得・表示するようにしてみます。
最初に properties ファイルを作成します。 src/main/resources の下に constant.properties を作成します。作成後、リンク先の内容 に変更します。
次に定数クラスを作成します。src/main/java/ksbysample/webapp/config の下に Constant.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下の CountryController.java を リンク先の内容 に変更します。
src/main/resources/templates/country の下の input.html を リンク先の内容 に変更します。
src/main/resources/templates/country の下の confirm.html を リンク先の内容 に変更します。
エラーメッセージの日本語化
今のままだとエラーメッセージが英語で表示されるので日本語化します。src/main/resources の下の ValidationMessages_ja_JP.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/basic/web の下の CountryForm.java を リンク先のその2の内容 に変更します。
動作確認
一旦この時点で動作を確認します。Gradle tasks View から bootRun タスクを実行します。
ブラウザで http://localhost:8080/country/input にアクセスして登録画面を表示します。
何も入力せずに「確認」ボタンをクリックします。Bean Validation が行われエラーメッセージが日本語で表示されます。
今度はエラーが出ないようデータを全て入力して「確認」ボタンをクリックします。確認画面が表示されます。
「戻る」ボタンをクリックします。入力画面に戻り、入力した値が表示されています。
- GovernmentForm に?が表示されているのは、全角の?を入力しているためです。エラーではありません。
- Code2 に値が表示されていないのは入力フィールドが小さすぎるからでした。。。
Ctrl+F2 を押して Tomcat を停止します。
次回は。。。
ここまで時間がかかったので一旦締めます。次回は以下の内容を実施する予定です。
- Code2 の入力フィールドが小さすぎるのでもう少し大きくします。
- Validator インターフェースを実装したクラスを用意して、複合項目の入力チェックも実装してみます。
- エラー時、入力フィールドの背景色を赤にしたいのでやり方を調べます。
- SurfaceArea と Population は @NotBlank, @Digits の2つのアノテーションを付加していますが、入力チェックは両方実行され、どちらのエラーメッセージも表示されていました。チェックの順序を指定して、1つのチェックがエラーになったら次のチェックは行わないようにする方法があるのか調べてみます。
- 書いてある以外にもいろいろ試しているのですが、SurfaceArea の入力チェックを @NotBlank, @Digits の2つではなく @Digits だけにしても空の時にエラーになる ( @Digits だけでも必須チェックが入る? ) ことに気づきました。@Digits だけなら空の時にはエラーにならないようにして欲しいのですが、原因を調査してみます。
ソースコード
CountryForm.java
■その1
package ksbysample.webapp.basic.web; import lombok.Data; import org.hibernate.validator.constraints.NotBlank; import javax.validation.constraints.Digits; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; @Data public class CountryForm { @NotBlank @Size(max = 3, message = "{error.size.max}") private String code; @NotBlank @Size(max = 52, message = "{error.size.max}") private String name; @NotBlank @Pattern(regexp = "^(Asia|Europe|North America|Africa|Oceania|Antarctica|South America)$", message = "{countryListForm.continent.pattern}") private String continent; @NotBlank @Size(max = 26, message = "{error.size.max}") private String region; @NotBlank @Digits(integer=8, fraction=2) private String surfaceArea; @NotBlank @Digits(integer=11, fraction=0) private String population; @NotBlank @Size(max = 45, message = "{error.size.max}") private String localName; @NotBlank @Size(max = 45, message = "{error.size.max}") private String governmentForm; @NotBlank @Size(max = 2, message = "{error.size.max}") private String code2; }
■その2
@NotBlank @Digits(integer=8, fraction=2, message = "{error.digits.integerandfraction}") private String surfaceArea; @NotBlank @Digits(integer=11, fraction=0, message = "{error.digits.integeronly}") private String population;
- surfaceArea の @Digists に
message = "{error.digits.integerandfraction}"
を追加します。 - population の @Digists に
message = "{error.digits.integeronly}"
を追加します。
constant.properties
continent.list=Asia,Europe,North America,Africa,Oceania,Antarctica,South America
Constant.java
package ksbysample.webapp.basic.config; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; import java.util.List; @Component @PropertySource("classpath:constant.properties") @Getter public class Constant { @Value("#{'${continent.list}'.split(',')}") private List<String> CONTINENT_LIST; }
- このクラスを使用するクラスにインジェクションして使用するので、@Component アノテーションを付加します。
- @PropertySource アノテーションで参照する properties ファイルを指定します。ここでは constant.properties を指定します。
- @Value アノテーションと Spring EL を使用して、設定ファイルの continent.list のデータを読み込んで List型の CONTINENT_LIST にセットします。
- 定数クラスで読み取り専用にするので lombok の @Getter アノテーションのみ定義します。
CountryController.java
@Controller @RequestMapping("/country") public class CountryController { @Autowired private Constant constant; @RequestMapping("/input") public String input(CountryForm countryForm , Model model) { model.addAttribute("continentList", constant.getCONTINENT_LIST()); return "country/input"; } @RequestMapping("/input/back") public String inputBack(CountryForm countryForm , RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("countryForm", countryForm); return "redirect:/country/input"; } @RequestMapping("/confirm") public String confirm(@Validated CountryForm countryForm , BindingResult bindingResult , Model model) { if (bindingResult.hasErrors()) { model.addAttribute("continentList", constant.getCONTINENT_LIST()); return "country/input"; } return "country/confirm"; }
- constnat フィールドを追加します。
- input メソッドの引数に
CountryForm countryForm
,Model model
を追加します。Bean Validation は行わないので @Validated アノテーションは付加しません。 - input メソッド内の処理に
model.addAttribute("continentList", constant.getCONTINENT_LIST());
を追加します。 - inputBack メソッドを追加します。確認画面で「戻る」ボタンが押された時に呼び出される処理です。
- confirm メソッドの引数に
@Validated CountryForm countryForm
,BindingResult bindingResult
を追加します。 - confirm メソッドの中に Bean Validation でエラーが発生した時の処理を追加します。
- Continent のドロップダウンリストにデータが表示されるようにするために
return "country/input";
の前に必ずmodel.addAttribute("continentList", constant.getCONTINENT_LIST());
を呼び出すようにしているのですが、毎回呼び出すのは良い方法とは思えないので、Thymeleaf テンプレートファイル内でクラスか properties ファイルから値を取得する等して解決できないか調査中です。今回は調査しきれませんでした。。。
input.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Countryデータ登録画面(入力)</title> <!-- Bootstrap core CSS --> <link href="/css/bootstrap.min.css" rel="stylesheet"/> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- Custom styles for this template --> <style> <!-- body { padding-top: 50px; } .navbar-brand { font-size: 24px; } .form-group { margin-bottom: 5px; } h1 { margin-bottom: 20px; } .cst-form-inputform { margin-bottom: 20px; } --> </style> </head> <body> <div id="header" th:include="common/header :: header (active='countryInput')"></div> <div class="container"> <div class="row"> <div class="col-sm-push-2 col-sm-10"><h1>Countryデータ登録</h1></div> </div> <div class="row"> <div class="col-xs-12"> <form id="countryForm" method="post" action="/country/confirm" th:action="@{/country/confirm}" th:object="${countryForm}" class="form-horizontal cst-form-inputform"> <div class="form-group" th:classappend="${#fields.hasErrors('*{code}')} ? 'has-error' : ''"> <label for="code" class="control-label col-sm-2">Code</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-2"><input type="text" name="code" id="code" class="form-control input-sm" value="" placeholder="JPN" th:field="*{code}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{code}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{code}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{name}')} ? 'has-error' : ''"> <label for="name" class="control-label col-sm-2">Name</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-4"><input type="text" name="name" id="name" class="form-control input-sm" value="" placeholder="Japan" th:field="*{name}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{name}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{name}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{continent}')} ? 'has-error' : ''"> <label for="continent" class="control-label col-sm-2">Continent</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-4"> <select name="continent" id="continent" class="form-control input-sm" th:field="*{continent}"> <option th:each="item : ${continentList}" th:value="${item}" th:text="${item}">continent</option> </select> </div></div> <div class="row" th:if="${#fields.hasErrors('*{continent}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{continent}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{region}')} ? 'has-error' : ''"> <label for="region" class="control-label col-sm-2">Region</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-4"><input type="text" name="region" id="region" class="form-control input-sm" value="" placeholder="Eastern Asia" th:field="*{region}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{region}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{region}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{surfaceArea}')} ? 'has-error' : ''"> <label for="surfaceArea" class="control-label col-sm-2">SurfaceArea</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-2"><input type="text" name="surfaceArea" id="surfaceArea" class="form-control input-sm" value="" placeholder="377829.00" th:field="*{surfaceArea}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{surfaceArea}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{surfaceArea}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{population}')} ? 'has-error' : ''"> <label for="population" class="control-label col-sm-2">Population</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-2"><input type="text" name="population" id="population" class="form-control input-sm" value="" placeholder="126714000" th:field="*{population}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{population}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{population}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{localName}')} ? 'has-error' : ''"> <label for="localName" class="control-label col-sm-2">LocalName</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-6"><input type="text" name="localName" id="localName" class="form-control input-sm" value="" placeholder="Nihon/Nippon" th:field="*{localName}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{localName}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{localName}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{governmentForm}')} ? 'has-error' : ''"> <label for="governmentForm" class="control-label col-sm-2">GovernmentForm</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-6"><input type="text" name="governmentForm" id="governmentForm" class="form-control input-sm" value="" placeholder="Constitutional Monarchy" th:field="*{governmentForm}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{governmentForm}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{governmentForm}"></small></p></div></div> </div> </div> <div class="form-group" th:classappend="${#fields.hasErrors('*{code2}')} ? 'has-error' : ''"> <label for="code2" class="control-label col-sm-2">Code2</label> <div class="col-sm-10"> <div class="row"><div class="col-sm-1"><input type="text" name="code2" id="code2" class="form-control input-sm" value="" placeholder="JP" th:field="*{code2}"/></div></div> <div class="row" th:if="${#fields.hasErrors('*{code2}')}"><div class="col-sm-10"><p class="form-control-static text-danger"><small th:errors="*{code2}"></small></p></div></div> </div> </div> <div class="col-xs-12"> </div> <div class="text-center"> <button type="button" id="js-confirm" value="confirm" class="btn btn-primary">確認</button> </div> </form> </div> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="/js/jquery.min.js"></script> <script src="/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/js/ie10-viewport-bug-workaround.js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $('#code').focus(); $('#js-confirm').bind('click', function(){ $('#countryForm').submit(); }); }); --> </script> </body> </html>
- 確認ボタンが画面の下にピッタリくっついていることに気づいたので、少し余白を開けるために
.cst-form-inputform { ... }
の定義を追加します。 <form id="countryForm" ...>
にth:action="@{/country/confirm}" th:object="${countryForm}"
を追加します。また class 属性をclass="form-horizontal"
→class="form-horizontal cst-form-inputform"
へ変更します。- 各入力項目の
<div class="form-group" ...>
にth:classappend="${#fields.hasErrors('*{code}')} ? 'has-error' : ''"
を追加します。エラー発生時に入力項目名と入力フィールドが赤くなります。 - 各入力項目の下にエラーメッセージが表示されるよう修正します。
- Continent の入力項目は select タグ、option タグを使用してドロップダウンリストになるよう修正します。
confirm.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Countryデータ登録画面(確認)</title> <!-- Bootstrap core CSS --> <link href="/css/bootstrap.min.css" rel="stylesheet"/> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- Custom styles for this template --> <style> <!-- body { padding-top: 50px; } .navbar-brand { font-size: 24px; } .form-group { margin-bottom: 5px; } h1 { margin-bottom: 20px; } .form-control-static { padding-top: 5px; padding-bottom: 5px; } --> </style> </head> <body> <div id="header" th:include="common/header :: header (active='countryInput')"></div> <div class="container"> <div class="row"> <div class="col-sm-push-2 col-sm-10"><h1>Countryデータ登録</h1></div> </div> <div class="row"> <div class="col-xs-12"> <form id="countryForm" method="post" action="/country/update" th:action="@{/country/update}" th:object="${countryForm}" class="form-horizontal"> <div class="form-group"> <label for="code" class="control-label col-sm-2">Code</label> <div class="col-sm-2"> <p class="form-control-static" th:text="*{code}">JPN</p> </div> </div> <div class="form-group"> <label for="name" class="control-label col-sm-2">Name</label> <div class="col-sm-4"> <p class="form-control-static" th:text="*{name}">Japan</p> </div> </div> <div class="form-group"> <label for="continent" class="control-label col-sm-2">Continent</label> <div class="col-sm-4"> <p class="form-control-static" th:text="*{continent}">Asia</p> </div> </div> <div class="form-group"> <label for="region" class="control-label col-sm-2">Region</label> <div class="col-sm-4"> <p class="form-control-static" th:text="*{region}">Eastern Asia</p> </div> </div> <div class="form-group"> <label for="surfaceArea" class="control-label col-sm-2">SurfaceArea</label> <div class="col-sm-2"> <p class="form-control-static" th:text="*{surfaceArea}">377829.00</p> </div> </div> <div class="form-group"> <label for="population" class="control-label col-sm-2">Population</label> <div class="col-sm-2"> <p class="form-control-static" th:text="*{population}">126714000</p> </div> </div> <div class="form-group"> <label for="localName" class="control-label col-sm-2">LocalName</label> <div class="col-sm-6"> <p class="form-control-static" th:text="*{localName}">Nihon/Nippon</p> </div> </div> <div class="form-group"> <label for="governmentForm" class="control-label col-sm-2">GovernmentForm</label> <div class="col-sm-6"> <p class="form-control-static" th:text="*{governmentForm}">Nihon/Constitutional Monarchy</p> </div> </div> <div class="form-group"> <label for="code2" class="control-label col-sm-2">Code2</label> <div class="col-sm-1"> <p class="form-control-static" th:text="*{code2}">JP</p> </div> </div> <input type="hidden" name="code" id="code" th:value="*{code}"/> <input type="hidden" name="name" id="name" th:value="*{name}"/> <input type="hidden" name="continent" id="continent" th:value="*{continent}"/> <input type="hidden" name="region" id="region" th:value="*{region}"/> <input type="hidden" name="surfaceArea" id="surfaceArea" th:value="*{surfaceArea}"/> <input type="hidden" name="population" id="population" th:value="*{population}"/> <input type="hidden" name="localName" id="localName" th:value="*{localName}"/> <input type="hidden" name="governmentForm" id="governmentForm" th:value="*{governmentForm}"/> <input type="hidden" name="code2" id="code2" th:value="*{code2}"/> <div class="col-xs-12"> </div> <div class="text-center"> <button type="button" id="js-update" value="update" class="btn btn-primary">登録</button> <button type="button" id="js-back" value="back" class="btn btn-default">戻る</button> </div> </form> </div> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="/js/jquery.min.js"></script> <script src="/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/js/ie10-viewport-bug-workaround.js"></script> <script type="text/javascript"> <!-- $(document).ready(function() { $('#js-update').bind('click', function(){ $('#countryForm').submit(); }); $('#js-back').bind('click', function(){ $('#countryForm').attr('action', '/country/input/back'); $('#countryForm').submit(); }); }); --> </script> </body> </html>
.form-control-static { ... }
の CSS を追加します。入力画面から確認画面に遷移した時に入力値の表示位置が縦にずれるのを修正します。<form id="countryForm" ...>
にth:action="@{/country/update}" th:object="${countryForm}"
を追加します。- 入力値を表示する箇所に
th:text="*{...}"
を追加します。 - 入力値を保持するための
<input type="hidden" ...
を追加します。 - 「戻る」ボタンがクリックされたら /country/input/back を呼び出すよう
$('#js-back').bind('click', function(){ ... }
内の URL を変更します。
ValidationMessages_ja_JP.properties
org.hibernate.validator.constraints.NotBlank.message=必須の入力項目です。 error.size.max = {max}文字以内で入力して下さい。 error.digits.integerandfraction = 数値を整数{integer}桁以内、小数{fraction}桁以内で入力して下さい。 error.digits.integeronly = 数値を整数{integer}桁以内で入力して下さい。 countryListForm.continent.pattern = Asia, Europe, North America, Africa, Oceania, Antarctica, South America のいずれかの文字列を入力して下さい。
- 以下の3つを追加します。
履歴
2015/02/12
初版発行。