4.12. Thymeleafにおける画面レイアウト

4.12.1. Overview

4.12.1.1. Thymeleafのテンプレートレイアウト機能を使用したHTMLの部品化

Thymeleafのテンプレートレイアウト機能を使用すると共通的に使用するHTMLを部品化することが出来る。
Thymeleafでは、共通的なHTMLを部品化して使用するための以下の機能がある。
HTML部品の定義と参照のための機能
項番 属性 説明
th:fragment
HTMLを部品化するための属性。部品化されたHTMLはフラグメントと呼ぶ。
th:insert
フラグメントを参照するための属性。
指定したタグに参照したフラグメントを挿入することができる。指定したタグ自体は残るが、そのタグの子要素は残らない。
th:replace
フラグメントを参照するための属性。
指定したタグを参照したフラグメントで置換することができる。指定したタグ自体も残らない。

Tip

上記以外に、th:include 属性が使用可能だが、Thymeleaf3.2で削除される予定のため推奨しない。 th:include 属性を指定すると、th:fragment 属性を設定したタグの子要素のみが挿入される。 th:include 属性と同様の処理を行いたい場合は、th:fragment 属性を設定したタグに、 th:remove="tag" を設定した上で、th:insert 属性を使用することで実現可能である。

また、HTMLの部品化と関連して、フラグメント式という機能がある。
フラグメント式は変数式などと同様にテンプレートHTML内で使用することができ、テンプレートHTMLの断片を取得することができる。
th:insert 属性、th:replace 属性と組み合わせて使うことで、フラグメントやテンプレートHTMLの一部を他のテンプレートHTMLに埋め込むことができる。
フラグメント式の記述パターン
項番 説明
~{Viewのパス :: セレクタ}
Viewのパス で指定されたテンプレートHTMLから、指定されたセレクタ に該当する要素を特定し取得する。
~{Viewのパス :: フラグメント名}
Viewのパス で指定されたテンプレートHTMLから、指定されたフラグメント名 に一致するth:fragment 属性が設定された要素を特定し取得する。
~{Viewのパス}
Viewのパス で指定されたテンプレートHTML全体を取得する。
~{:: セレクタ}
フラグメント式が記載されているテンプレートHTML自体から、指定されたセレクタ に該当する要素を特定し取得する。
セレクタ の代わりにフラグメント名 を指定することも可能である。
~{this :: セレクタ}
項番4の別の記載方法である。

Note

フラグメント式は、セレクタを使って自由に他のテンプレートHTMLの一部を取得できてしまうので、多用はHTMLの構造を複雑化させる。 そのためプロジェクト全体で共通部品として何をフラグメント化するかを決めて使用することが推奨される。

th:fragment属性を利用して定義したフラグメントは、th:fragment="フラグメント名 (param1,param2)"のようにパラメータを取ることが可能である。
パラメータを取ることでフラグメントの汎用性を高めることが可能となり、HTML部品の共通化が容易となる。
パラメータ化されたフラグメントを取得する場合は、~{Viewのパス :: フラグメント名 (${address},true)}のようにフラグメント式のフラグメント名の後ろに、パラメータとして渡す値を記述する。
パラメータとして渡す値には、変数式(${...})やリテラル(文字列、真偽値など)のほか、フラグメント式(~{...})を指定することも可能である。
How to useで解説する実装例では、パラメータとしてフラグメント式を渡す方法を利用している。

Note

パラメータを渡す際に、~{Viewのパス :: フラグメント名 (param1=${address},param2=true)}のようにパラメータ名を明示的に記述することも可能である。 この場合、パラメータを渡す順序はフラグメントで定義したパラメータの順序と異なっていても問題ない。 なお、本ガイドラインでは可読性と記述の簡潔さを重視し、パラメータ名を記述しない方法を推奨する。

上記で紹介したテンプレートレイアウト機能によるHTMLの部品化を応用して共通的な画面レイアウトを作成する方法を次節以降で説明する。
なお、実際に業務アプリケーションを作成する場合、共通的な画面レイアウトの構成以外にも画面を構成するHTMLの一部について汎用的なHTML部品(フラグメント)を作成することが考えられる。
その方法については、「汎用的なHTML部品の作成方法」 で解説する。

4.12.1.2. 共通的な画面レイアウトの作成

ヘッダ、フッタ、サイドメニューといった共通的なレイアウトを持つWebアプリケーションを開発する場合に、全てのテンプレートHTMLに共通部分をコーディングすると、メンテナンスが煩雑になる。
例えば、ヘッダのデザインを修正する必要がある場合、全てのテンプレートHTMLに修正を加えなければならない。
Thymeleafでは、テンプレートレイアウト機能を使うことで、Apache Tilesのように統一的なレイアウトを構成することができる。
多くの画面で同じレイアウトを使用する場合は、Thymeleafのテンプレートレイアウト機能を使用して、統一的な画面レイアウトを定義して、個別の画面に適用することを推奨する。
理由は、以下3つの通りである。
  1. 設計者によるレイアウトの誤差をなくすこと
  2. 冗長なコードを減らすこと
  3. 大きなレイアウトの変更が容易になること
統一的な画面レイアウトの定義を行うことで、別々のテンプレートHTMLを組み合わせることができる。
その結果、各々のテンプレートHTMLに、余計なコードを記述することがなくなるため、開発者の作業を楽にできる。
例えば、下記のようなレイアウト構成が複数の画面に存在する場合、
screen layout

Picture - Image of screen layout

統一的な画面レイアウトの定義を行うことで、同じレイアウトの全ての画面でheaderやmenu、footerを挿入してサイズを指定することなく、bodyの作成のみに集中することができる。
実際のHTMLファイルは下記のようになる。
layout html

Picture - Image of layout html

よって、統一的な画面レイアウトを定義した後は、業務に相当するHTMLファイルのみ(business.html)画面毎に作成すればよい。

Note

Thymeleafのテンプレートレイアウト機能を使用した統一的な画面レイアウトを適用しない方がよい場合もある。例えば、エラー画面に統一的な画面レイアウトを使用するのは、以下の理由により推奨しない。

  • エラー画面表示中に共通的なレイアウトの部分にエラーが発生すると解析がしにくくなるため。(二重障害発生の場合)

4.12.2. How to use

4.12.2.1. Thymeleafのテンプレートレイアウト機能を使用した画面レイアウト

4.12.2.1.1. レイアウト作成

以降、以下のファイル構成を前提に画面レイアウトの作成方法を示す。

  • File Path
WEB-INF
  └─views
     ├─layout
     │      header.html
     │      template.html
     │      footer.html

     └─staff
            createForm.html

レイアウトの枠となるHTMLファイル(template.html)と、レイアウトに埋め込むHTMLファイルを作成する。

  • template.html
<!DOCTYPE html>
<!--/* (1) */-->
<html class="no-js" xmlns:th="http://www.thymeleaf.org" th:fragment="layout (title,body)">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}" type="text/css"
    media="screen, projection">
<script type="text/javascript">

</script>
<!--/* (2) */-->
<title th:replace="${title}">Staff Management System</title>
</head>
<body>
    <!--/* (3) */-->
    <div id="header" th:replace="~{layout/header :: header}"></div>
    <!--/* (4) */-->
    <div id="body" th:replace="${body}"></div>
    <!--/* (5) */-->
    <div id="footer" th:replace="~{layout/footer :: footer}"></div>
</body>
</html>
項番 説明
(1)
htmlタグ以下全体をlayout という名前でフラグメント化し、個別のテンプレートHTMLと合成できるようにしている。
また、個別画面(createForm.html)のtitle タグとbody タグの内容をパラメータとして受け取っている。
(2)
th:replace 属性を使用して(1)でパラメータとして受け取った個別画面(createForm.html)のtitle タグの内容で置換している。
(3)
th:replace 属性を使用して後述するlayout/headar.htmlのheader フラグメントで置換している。
(4)
th:replace 属性を使用して(1)でパラメータとして受け取った個別画面(createForm.html)のbody タグ内のコンテンツで置換している。
(5)
th:replace 属性を使用し、後述するlayout/footer.htmlのfooter フラグメントで置換している。

Note

th:replace="~{layout/header :: header}" は、~{} を省略して、th:replace="layout/header :: header" と書くこともできるが、本ガイドラインでは可読性を重視し~{}を省略しないことを推奨する。

  • header.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>header</title>
</head>
<body>
    <!--/* (1) */-->
    <div th:fragment="header" th:remove="tag">
        <h1>
            <a th:href="@{/}">Staff Management System</a>
        </h1>
    </div>
</body>
</html>
項番 説明
(1)
th:fragment 属性を使用し、header という名前でフラグメント化している。
画面レイアウトを合成するなかで使用されるのはフラグメント化された部分のみ。
ここでは、単独でHTMLとして表示できるようにbody タグ以上のタグも記述し仮のタイトルを記述している。
また、th:replace属性を使用して合成したときにdiv タグを残さないため、th:remove 属性の値にtag を設定している。
  • createForm.html(body部分の例)

    開発者は、個別画面のコンテンツ(主にbody部分)のみに集中して記述できる。

<!DOCTYPE html>
<!--/* (1) */-->
<html xmlns:th="http://www.thymeleaf.org"
    th:replace="~{layout/template :: layout(~{::title},~{::body/content()})}">
<head>
<!--/* (2) */-->
<title>Create Staff Information</title>
</head>
<body>
    <h2>Create Staff Information</h2>
    <form th:object="${staffInfoForm}" method="post" th:action="@{/staff/create}">
        <table>
            <tr>
                <td>Staff First Name</td>
                <td><input type="text" th:field="*{firstName}"></td>
            </tr>
            <tr>
                <td>Staff Family Name</td>
                <td><input type="text" th:field="*{familyName}"></td>
            </tr>
            <tr>
                <td rowspan="5">Staff Authorities</td>
                <td><input type="checkbox" value="01" th:field="*{authorities}"> Staff Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="02" th:field="*{authorities}"> Master Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="03" th:field="*{authorities}"> Stock Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="04" th:field="*{authorities}"> Order Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="05" th:field="*{authorities}"> Show Shopping Management</td>
            </tr>
        </table>

        <input type="submit" value="cancel">
        <input type="submit" value="confirm">
    </form>
</body>
</html>
項番 説明
(1)
th:replace 属性を使用して、テンプレートであるlayout/template.htmlのlayout フラグメントの内容でhtml タグ以下の内容を置換している。
~{::title} は自身のテンプレートHTMLのtitle タグを、~{::body/content()} は自身のテンプレートHTMLのbody タグ内のコンテンツを取得している。
そして、自身のHTMLのtitle タグ、body タグをlayout/template.htmlのlayout フラグメントにパラメータとして渡している。
そのため、 画面レイアウトを合成するなかで使用されるのはパラメータとして渡したtitle タグ、body タグの内容のみである。
ここでは、単独でHTMLとして表示できるようにbody タグ以上のタグも記述し仮のタイトルを記述している。
(2)
template.htmlのtitle タグを置き換えるタイトルメッセージを定義している。

Note

上記の実装例に示した通り、適用するテンプレートを画面ごとに指定する形になっているので、指定するテンプレートを変えることで別のレイアウトを適用することができる。 複数のテンプレートレイアウトを使い分けることで、同一アプリケーション内で異なる画面レイアウトに対応することが可能となっている。

Note

個別画面でtitle タグを定義せず、テンプレートのtitle タグの内容をそのまま使用する場合は、no-operation token(“_” ) を使用して、th:replace="~{layout/template :: layout(_,~{::body/content()})}" と記述する。 no-operation tokenについては、条件を判定するのNoteも参照されたい。

  • footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>footer</title>
</head>
<body>
    <div th:fragment="footer" th:remove="tag">
        <!--/* (1) */-->
        <p style="text-align: center; background: #e5eCf9;">Copyright &copy; 20XX CompanyName</p>
    </div>
</body>
</html>
項番 説明
(1)
th:fragment 属性を使用し、footer という名前でフラグメント化している。
画面レイアウトを合成するなかで使用されるのはフラグメント化された部分のみである。
ここでは、単独でHTMLとして表示できるようにbody タグ以上のタグも記述し仮のタイトルを記述している。
また、合成後にdiv タグを残さないため、th:remove 属性の値にtag を設定している。

Note

フッターに記載する著作権に関しては 画面フッターの著作権 を参照すること。

結果として上記のtemplate.htmlに、header.html、createForm.html、footer.htmlが組み合わされた方法でブラウザに出力される。

thymeleaf result

出力されるHTMLは以下の通り。

<!DOCTYPE html>

<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="/staff-management-web/resources/app/css/styles.css" type="text/css"
    media="screen, projection">
<script type="text/javascript">

</script>

<title>Create Staff Information</title>
</head>
<body>


    <h1>
        <a href="/staff-management-web/">Staff Management System</a>
    </h1>



    <h2>Create Staff Information</h2>
    <form method="post" action="/staff-management-web/staff/create"><input type="hidden" name="_csrf" value="2557dc95-6f36-4c2c-9900-9e0efd411ad7">
        <table>
            <tr>
                <td>Staff First Name</td>
                <td><input type="text" id="firstName" name="firstName" value=""></td>
            </tr>
            <tr>
                <td>Staff Family Name</td>
                <td><input type="text" id="familyName" name="familyName" value=""></td>
            </tr>
            <tr>
                <td rowspan="5">Staff Authorities</td>
                <td><input type="checkbox" value="01" id="authorities1" name="authorities"><input type="hidden" name="_authorities" value="on"> Staff Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="02" id="authorities2" name="authorities"><input type="hidden" name="_authorities" value="on"> Master Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="03" id="authorities3" name="authorities"><input type="hidden" name="_authorities" value="on"> Stock Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="04" id="authorities4" name="authorities"><input type="hidden" name="_authorities" value="on"> Order Management</td>
            </tr>
            <tr>
                <td><input type="checkbox" value="05" id="authorities5" name="authorities"><input type="hidden" name="_authorities" value="on"> Show Shopping Management</td>
            </tr>
        </table>

        <input type="submit" value="cancel">
        <input type="submit" value="confirm">
    </form>




    <p style="text-align: center; background: #e5eCf9;">Copyright &copy; 20XX CompanyName</p>

</body>
</html>

Note

上記の例では、body タグ内のコンテンツ以外にtilte タグを渡しているが、フラグメントに引数を追加することで任意のパラメータを個別画面からテンプレートに渡すことができる。
以下はscript タグの例である。
  • template.html
<!DOCTYPE html>
<!--/* (1) */-->
<html class="no-js" xmlns:th="http://www.thymeleaf.org" th:fragment="layout (title,script,body)">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" th:href="@{/resources/app/css/styles.css}" type="text/css"
    media="screen, projection">
<!--/* (2) */-->
<script type="text/javascript" th:replace="${script}">
</script>
<title th:replace="${title}">Staff Management System</title>
</head>
<!-- omitted -->
</html>
項番 説明
(1)
layout フラグメントに、個別画面のscript タグの内容をパラメータとして受け取るための引数script を追加する。
(2)
th:replace 属性を使用して、(1)でパラメータとして受け取ったscript タグの内容で置換する。
なお、タグごと置換されるのでscript タグ以外のダミーのタグに同様の設定をしても問題なく動作する。
  • createForm.html(body部分の例)
<!DOCTYPE html>
<!--/* (1) */-->
<html xmlns:th="http://www.thymeleaf.org"
    th:replace="~{layout/template :: layout(~{::title},~{::script},~{::body/content()})}">
<head>
<title>Create Staff Information</title>
<!--/* (2) */-->
<script type="text/javascript" th:src="@{/resources/app/js/sample.js}"></script>
</head>
<body>
    <h2>Create Staff Information</h2>
    <!-- omitted -->
</html>
項番 説明
(1)
th:replace 属性のlayout フラグメントを指定している箇所にパラメータとしてフラグメント式~{::script} を追加する。
(2)
個別画面固有のscript タグを定義し、使用するJavaScriptファイルを読み込んでいる。

Warning

テンプレートレイアウトにより部品化する際に、フラグメント(部品)に引数として式を与える場合には、式を th:object と選択変数式 *{} ではなく、変数式 ${} で組み立てなければならない。 これは、部品内で th:object と選択変数式 *{} を使用していても、部品を呼び出す側が与えた式の意味(参照するもの)が変わらないようにするための(裏を返せば、部品内での th:object と選択変数式 *{} の使用を制限せずに済むための)ルールである。 こちらの詳細については、オブジェクトのプロパティを省略して指定するのWarningを参照されたい。

4.12.3. How to extend

4.12.3.1. 汎用的なHTML部品の作成方法

th:fragment属性を使用して汎用的なHTML部品(フラグメント)を作成することができる。
本節では、結果メッセージを表示するHTMLを再利用するため、HTML部品(フラグメント)化する例を示す。

Model に格納された org.terasoluna.gfw.common.message.ResultMessages を参照し、結果メッセージを出力するフラグメントを作成する。
  • common-parts.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>common-parts</title>
</head>
<body>
    <!--/* (1) */-->
    <div th:fragment="messagesPanel" th:if="${resultMessages} != null"
        th:class="|alert alert-${resultMessages.type}|">
        <ul>
            <!--/* (2) */-->
            <li th:each="message : ${resultMessages}"
                th:text="${message.code} != null ? ${#messages.msgWithParams(message.code, message.args)} : ${message.text}">blank
                messages</li>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
th:fragment 属性を使用し、messagesPanel という名前でフラグメント化する。
th:if 属性は、条件に応じて、タグを生成するかどうか制御するための属性である。
ここでは、resultMessagesnull の場合に、結果メッセージを表示するHTMLを生成しないようにしている。
また、th:class 属性を使用し、ResultMessages に設定されたメッセージタイプ(例: info, error)に応じたclass 属性を設定する。
(2)
th:each 属性は、コレクションや配列に対して繰り返し処理を行うための属性である。
ResultMessages には、複数件の結果メッセージを格納できるので、それを1件づつ取得し、li タグを生成している。
メッセージの取得はThymeleafの#messages を使用する。
その#messages.msgWithParams({メッセージID},{置換文字列}) メソッドを使用することで、プロパティファイルからメッセージを取得することができる。

フラグメント化したmessagesPanelを各テンプレートHTMLにおいて使用する。
  • businessError.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Business Error!</title>
<link rel="stylesheet"
    href="../../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h1>Business Error!</h1>
        <div class="error">
            <span th:text="${#strings.isEmpty(exceptionCode)} ? #{e.xx.fw.8001} : |[${exceptionCode}] #{${exceptionCode}}|"></span>
            <!-- (1) -->
            <span th:replace="~{common/common-parts :: messagesPanel}"></span>
        </div>
        <br>
        <!-- omitted -->
    </div>
</body>
</html>
項番 説明
(1)
th:replace 属性を使用して、”messagesPanel”フラグメントの内容で置換している。

org.terasoluna.gfw.common.message.ResultMessages に結果メッセージが格納されている場合、結果として、businessError.htmlの一部にmessagesPanelの内容が埋め込まれた形でブラウザに出力される。

thymeleaf result

出力されるHTMLは以下の通り。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Business Error!</title>
<link rel="stylesheet"
    href="/staff-management-web/resources/app/css/styles.css" type="text/css">
</head>
<body>
    <div id="wrapper">
        <h1>Business Error!</h1>
        <div class="error">
            <span>[e.xx.fw.8001] Business error occurred!</span>
            <div class="alert alert-error">
        <ul>

            <li>Create Staff error occurred!</li>
        </ul>
    </div>
        </div>
        <br>
        <!-- omitted -->
    </div>
</body>
</html>