Chapter 03

掲示板の入力フォームを作る

あしたば
あしたば
2021.03.22に更新

この章で学ぶこと

SpringBootで自由にHTMLを表示できることがわかりました。
けれど、今実現できているのは、サーバからユーザへ単純なデータを返すことのみです。

掲示板であるからには、ユーザが入力したデータをDBに保存し、保存した内容をユーザに返す必要があるでしょう。

この章では、thymeleafを使って最低限度の入力フォームの体裁を整えることにします。
thymeleafを自由に使いこなせるなら、この章を読む必要はありません。

  • レイアウトを整える
  • 入力フォームを作る
  • POSTしたデータをデバッグで確認してみる

参考リンク

thymeleafについては、日本語訳されたチュートリアルがあります。

レイアウトを整える

Webページのデザインはある程度の枠組みが決まっています。
Javaの内容から逸れるため、この点にあまり力を入れることはしませんが、下記の様なデザインパターンがあることは知っておきましょう。詳細は述べませんので、それぞれ検索してみてください。

  • 1カラムレイアウト
  • マルチカラムレイアウト
    • 2カラム
    • 3カラム
  • フルスクリーンレイアウト

等々。

今回は、某ちゃんねるのデザイン(PC/スレッド一覧)のように、2カラムレイアウトを採用してみましょう。

webページの構成

2カラムレイアウトは大きく4つのパーツに分類されます。

  • ヘッダ
  • メイン
  • サイドバー
  • フッター

です。このうち、ページ遷移で内容がガラッとかわるのはメインのみであることがほとんどです。
ヘッダーはグローバルナビゲーションの役割を果たすため、主要なページ内リンクを置くことが多いです。
サイドバーは主要コンテンツの細分化リンクや広告が置かれることが多いです。
フッターは著作権表記やプライバシーポリシーなどが記載されることが多いようです。

さて、今回作成する掲示板においても、メインのみを切り替えることにしましょう。
このようなデザインでも、Thymeleafを使うことで、HTMLをベタ書きするよりはよっぽど効率的に作ることができます。

Thymeleaf Layout Dialectd

ThymeleafにはHTMLを部品化する機能がいくつかありますが、そのうち1つにThymeleaf Layout Dialectdがあります。
これは、いわばデフォルトのデザインテンプレートを作ることができる機能です。
今回の場合、ヘッダ、サイドバー、フッターを共通部品として利用し、メインのHTMLだけ入れ替えるような実装を可能にします。

インストール

では、pom.xmlを開いて、dependenciesにthymeleaf-layout-dialectを追加しましょう。

<dependency>
	<groupId>nz.net.ultraq.thymeleaf</groupId>
	<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

ライブラリを追加したあとはmavenにpom.xmlを読み込み直してもらいます。

このボタンを押せば、追記したライブラリがインストールされます。

レイアウトの利用

Thymeleaf Layout Dialectdでは、デコレータと呼ぶ共通パーツのテンプレートを使います。
まずこれを作ってみましょう。

/src/main/resources/templates/layout.htmlを作成します。

内容は次のとおりです。

<!DOCTYPE html>
<html
    xmlns         = "http://www.w3.org/1999/xhtml"
    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
    <meta charset="UTF-8" />
    <meta name="description" content="common-meta">
    <title>レイアウト</title>
</head>
<body>
    <main id="main-area" layout:fragment="layoutContent">
        <p>Default content</p>
    </main>
    <div id="side-area">
        サイドバー
    </div>
</body>
</html>

HTMLタグにあるxmlnsはThymeleaf向けの記述のためのおまじないであると公式チュートリアルに記載されています。

ここでは、layout:fragmentが挙動を理解するための鍵になります。

続いて、hello.htmlを次のように書き直してみましょう。

<!DOCTYPE html>
<html
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout}">
<head>
    <title>Hello world</title>
    <meta name="description" content="Hello">
</head>
<body>
<div layout:fragment="layoutContent">
    <P>Hello World</P>
</div>
</body>
</html>

これで再コンパイルし、helloページを確認します。すると、下記のように表示されます。

ブラウザの検証ツールでHTMLを確認すると、次のようになります。

さて、thymeleafのコードと、出力されたHTMLを見比べてみましょう。何が起こったのでしょうか。

layoutの効果

起きたことは2つです。

layoutのheadと、helloのheadがマージされている

layout.htmlのheadは次のとおりです。

<!-- layout.htmlの一部 -->
<head>
    <meta charset="UTF-8" />
    <meta name="description" content="common-meta">
    <title>レイアウト</title>
</head>

hello.htmlは次のとおりです。

<!-- hello.htmlの一部 -->
<head>
    <title>Hello world</title>
    <meta name="description" content="Hello">
</head>

そして、実際の出力は、

<head>
    <title>Hello world</title>
    <meta charset="UTF-8">
    <meta name="description" content="common-meta">
    <meta name="description" content="Hello">
</head>

このとおりです。

  • metaはマージ(一つにまとめること)されている
  • titleはlayout.htmlでなく、hello.htmlに上書きされている

このように、headに関してはレイアウトよりも個別ページを優先する形でマージが行われます。

レイアウトに個別ページの内容が注入されている

レイアウトのhtmlで、bodyに当たる内容は下記でした。

<!-- layout.htmlの一部 -->
<body>
    <main id="main-area" layout:fragment="layoutContent">
        <p>Default content</p>
    </main>
    <div id="side-area">
        サイドバー
    </div>
</body>

しかし、実際には

<body>
    <div id="main-area">
        <p>Hello World</p>
    </div>
    <div id="side-area">
        サイドバー
    </div>
</body>

このように表示されています。

レイアウトにあるlayout:fragment="layoutContent"のHTMLブロックに、hello.htmlのlayout:fragment="layoutContent"のHTMLブロックが注入され、上書きしていることがわかるでしょうか。

レイアウト側のlayout:fragment="layoutContent"とは、置換することのできるlayoutContentという名前のエリアであることを示しています。

hello.html側では、layout:decorate="~{layout}"によってlayout.htmlをベースに利用することを決めた上で、layout:fragment="layoutContent"という名前のエリアを宣言しています。

これにより、layout.html側の同名の名前のエリア(layoutContent)に、個別ページのlayoutContentを注入しているのです。

このように、Thymeleaf Layout Dialectdをつかうと、Webページの共通部品を定義することができ、掲示板ページ、アバウトページ、FAQページなど複数画面を作るときに、それぞれのページだけをコーディングすればよくなります。

デザインする

では枠組みをデザインします。

2カラムレイアウトにしたいので、layout.htmlのbodyを次のように書き換えます。

<body>
    <header>header</header>
    <div id="content">
        <main id="main-area" layout:fragment="layoutContent">
            <p>Default content</p>
        </main>
        <div id="side-area">
            サイドバー
        </div>
    </div>
    <footer>footer</footer>
</body>

headerはヘッダーを、<div id="content">はmainエリアとサイドバーを、footerはフッターの描画をそれぞれ担当させます。

次に、CSSをSpringBootに組み込むために、下記ディレクトリにCSSファイルを作成しましょう。

内容はこのようにします。

body {
    margin: 0;
}
header{
    display: flex;
    background-color: #f0f8ff;
    padding: 0.5rem 1rem;
}

#content {
    display: flex;
    background-color: #dae0e6;
    padding: 0.5rem 1rem;
}

#main-area {
    width: 60%;
}
#side-area {
    width: 25%;
}

footer {
    display: flex;
    background-color: #f0f8ff;
    padding: 0.5rem 1rem;
}

そして、このCSSをHTMLにリンクしてもらいます。
layout.htmlのheadに1行足してください。

<head>
    <meta charset="UTF-8" />
    <meta name="description" content="common-meta">
    <title>レイアウト</title>
    <!-- ↓これを足す↓-->
    <link th:href="@{/css/style.css}" rel="stylesheet" type="text/css">
</head>

これで、再コンパイルし表示すると次のような表示になっているはずです。

非常に質素で中身もないですが、区分けはできましたね。

このように、flexboxを用いると、簡単にレイアウトを組むことができます。(GridレイアウトやBootstrapという候補もあり得るでしょう)

さあ、ここからデザインの肉付け、、と行きたいところですが主題からそれますので、簡単なフォームをデザインして終わりとしましょう。

入力フォームを作る

HTMLを表示させるには、まずControllerを作る必要がありました。

今回は、それに加えてフォームがありますから、HTMLのフォームとJavaのデータを簡単に対応付ける方法に関して説明しておきます。

まず、今回作成するフォームでは下記のデータを送信することにします。

  • 名前
  • メールアドレス
  • コメント内容

このデータを扱うためのクラスを作りましょう。

まず、applicationというパッケージを作成しましょう。

さらに、formパッケージを作成し、CommentFormクラスを作成します。
内容は次のとおりです。

package chalkboard.me.bulletinboard.application.form;

import lombok.Data;

@Data
public class CommentForm {
  private String name;
  private String mailAddress;
  private String comment;
}

lombokの@Dataアノテーションは、コンストラクタとアクセサ(getter/setter)を同時に作成する強力なアノテーションです。
その名が示すとおり、今回のようなデータを受け渡すためのオブジェクトのみに使いましょう。濫用してはいけません。
(Setterはなるべく使用を避けるべきです。現場で役立つシステム設計の原則を読んでみてください)

では、BoardControllerを作成しましょう。

package chalkboard.me.bulletinboard.presentation;

import chalkboard.me.bulletinboard.application.form.CommentForm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequiredArgsConstructor
public class BoardController {

  @GetMapping("/board")
  public ModelAndView viewBoard(ModelAndView modelAndView){
    modelAndView.setViewName("board");
    modelAndView.addObject("commentForm", new CommentForm());
    return modelAndView;
  }
  
  @PostMapping("/board")
  public String postComment(@ModelAttribute CommentForm comment) {
    return "board";
  }
}

HTML、つまりView層にデータを渡す場合、ModelAndViewというクラスを利用します。

  • setViewNameはHTMLファイルのパスの設定を行っています
  • addObjectはThymeleafにデータを渡しています

これによって、thymeleafに空のフォームデータを渡すことができました。
それがどのように役立つかは、下記で説明します。

フォームをデザインする

某チャンネルでは掲示板のページはシングルカラムなのですが、今回は2カラムのまま進めてしまいます。

board.htmlを新たに作成しましょう。

内容はこのようにします。

<!DOCTYPE html>
<html
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.w3.org/1999/xhtml"
        layout:decorate="~{layout}">
<head>
    <title>掲示板</title>
    <meta name="description" content="掲示板です">
    <link th:href="@{/css/board.css}" rel="stylesheet" type="text/css">
</head>
<body>
<div layout:fragment="layoutContent">
    <div id="board-area">
        投稿の内容を表示する場所
    </div>
    <hr>
    <form method="POST" action="/board" th:object="${commentForm}">
        <input placeholder="名前" th:field="*{name}">
        <input placeholder="メールアドレス" th:field="*{mailAddress}">
        <textarea placeholder="コメント内容" rows="5" cols="80" wrap="off" th:field="*{comment}">
        </textarea>
        <button>送信</button>
    </form>
</div>
</body>
</html>

続いて、CSSを作成しましょう。

内容はこれだけです。

form {
    display: flex;
    flex-direction: column;
}

この段階で表示すると、下記のような画面になっているはずです。

HTMLフォームのデータとJavaの関連

今回作成したフォームに着目してみましょう。

    <form method="POST" action="/board" th:object="${commentForm}">
        <input placeholder="名前" th:field="*{name}">
        <input placeholder="メールアドレス" th:field="*{mailAddress}">
        <textarea placeholder="コメント内容" rows="5" cols="80" wrap="off" th:field="*{comment}">
        </textarea>
        <button>送信</button>
    </form>

th:object="${commentForm}" は、ControllerでaddObjectしたフォームクラスの名前を指定しています。これは、このフォームをJavaのフォームクラスと対応させることを意味しています。

th:field="*{name}" は、CommentFormクラスの各フィールドを指定しています。これによって、inputタグと、Javaのフォームクラスが互いに関連することになります。

では、どのように関連付けられるか、生成されるHTMLを見てみましょう。

<!-- 描画されたフォーム-->
<form method="POST" action="/board">
        <input placeholder="名前" id="name" name="name" value="">
        <input placeholder="メールアドレス" id="mailAddress" name="mailAddress" value="">
        <textarea placeholder="コメント内容" rows="5" cols="80" wrap="off" id="comment" name="comment"></textarea>
        <button>送信</button>
    </form>

input、textareaを見ると、idnameが付与されているのがわかります。

このうちnameが重要な役割を担います。

フォームを送信したとき、どのようなデータが送信されるか

HTML上でフォームを送信するときどのようにデータが送信されているのでしょうか。

その様子を検証ツールを使ってみることができます。

掲示板を開いて、ブラウザの検証ツールを開きます。
検証ツール(インスペクタ)の起動方法はブラウザによって違うので、調べてみてください

Chromeなら、Networkタブを開きます。

このまま、フォームから情報を送ってみます。

今回はこのような情報を入力しました。

ネットワークタブにboardと表示があるので、それをクリックします。

このような表示になるでしょう

下にスクロールすると、送られたデータが載っています。

つまり、formタグ内のデータをサーバに送信するとき、そのデータにつけられる名前がnameで指定されているのです。
そして、Java側で読み込むときも、このnameに沿った名前を読み込むことでデータを受け取れているのです。

thymeleafにフォームクラスのインスタンスを渡すことで、仮に扱っている情報の名前が変わっても追従できるようにしてくれているということです。

POSTしたデータをデバッグで確認してみる

ブラウザからデータが送信されていることはわかりました。

では、SpringBoot(Java)がどのようにデータを受け取っているかを見ていきましょう。

フォームのデータ送信先

フォームがどこにデータを送信するかは、formタグで決めています。気づきましたか?
<form method="POST" action="/board">
これは、HTTPメソッドPOSTで、http://localhost:8080/boardに送信せよ、の意味です。

すでに/boardはGETメソッドでも使っているパスですが、メソッドさえ違えば使い分けることができます。

Controllerの確認

では、POSTで送られたデータはどこで受けられるのでしょうか。

しれっとBoardControllerに記載されている下記のメソッドに注目しましょう。

  @PostMapping("/board")
  public String postComment(@ModelAttribute CommentForm comment) {
    return "board";
  }

@PostMappingによって、POSTメソッドで送信された/boardパスのリクエストを受けていることがわかります。

そして、引数にある@ModelAttributeがポイントになります。
IDEのデバッグで中身を見てみましょう。

IDEの行番号の少し右を押すと、赤い丸がつきます。

これがブレークポイントです。ここで処理を停止させることができます。

ブレークポイントを使うには、再生マークではなく「虫」のマークで起動する必要があります。

起動したら、掲示板のフォームに好きに入力して、情報を送ってみましょう。

IDEがこのようになったら成功です。

Variables(変数の意味)パネルで、今のスコープで有効な変数の値を見ることができます。

ブレークポイントの停止を解除するには、Debuggerパネルの再生ボタンを押します。いつもの右上ではないので注意しましょう。

不要になったブレークポイントは、赤丸をもう一度押して消しておきましょう。

これで、ブラウザから情報を送信し、サーバで受け取ることができました。

今回のPR

https://github.com/angelica-keiskei/spring-sample/pull/3