8テンプレートのレイアウト
8.1テンプレートフラグメントの有効化
フラグメントへの定義とリンク
テンプレートには、多くの場合、フッター、タイトル、メニューなど、他のテンプレートからのフラグメントが含まれます...
これを簡単にするために、Thymeleafでは、 th:fragment属性を使用して後で含めるためにこれらのフラグメントを定義する必要があります。
すべての製品ページに標準フッターを追加する場合、次のコードを含むファイル/WEB-INF/templates/footer.htmlを作成します。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </div> </body> </html>
上記のコードは、「copy」と呼ばれるスニペットを定義します。これは、 th:insertまたはth:replace属性のいずれかを使用してホームページに簡単に含めることができます( th:includeと同様に 、Thymeleaf 3.0では推奨されません):
<body> ... <div th:insert="~{footer :: copy}"></div> </body>
th:insertは、フラグメントの式(〜{...})がフラグメントにキャストされることを想定していることに注意してください。 ただし、複雑でないフラグメント式である例では、要素(〜{、})は完全にオプションであるため、上記のコードは同等です。
<body> ... <div th:insert="footer :: copy"></div> </body>
フラグメント構文
フラグメント式の構文は非常に簡単です。 3つの異なる形式があります。
- 〜{templatename :: selector} templatenameという名前のテンプレートに指定された「マークアップセレクター」を適用することで取得したフラグメントを含めます。 セレクターは単純なフラグメント名である可能性があるため、上記の〜{footer :: copy}のように〜{templatename :: fragmentname}のような単純なものを指定できます。
マークアップセレクターの構文は、AttoParserコア分析ライブラリによって決定され、XPath式またはCSSセレクターに似ています。 詳細については、付録Cを参照してください。
- 〜{templatename} templatenameという名前の完全なテンプレートが含まれます。 th:insert / th:replaceタグで使用するテンプレートの名前は、現在テンプレートエンジンで使用されているリゾルバーテンプレートを使用して解決する必要があることに注意してください。
- 〜{:: selector}または〜{this :: selector}セレクターに対応する同じテンプレートからフラグメントを挿入します。 式が表示されるテンプレートで見つからない場合、テンプレート呼び出し(挿入)のスタックは、セレクターがあるレベルで一致するまで、最初に処理されたテンプレートの方向(ルートへ)に移動します。 上記の例のテンプレート名とセレクターの両方は、次のようなフル機能の式(条件式でも可能です)です。
<div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>
繰り返しますが、ブロック〜{...}はth:insert / th:replaceではオプションです。
スニペットには、 th:*属性を含めることができます。 これらの属性は、フラグメントがターゲットテンプレート( th:insert / th:replace属性を含むもの)に含まれた後に実行され、このターゲットテンプレートで定義されたコンテキスト変数を参照できます。
フラグメントに対するこのアプローチの大きな利点は、Thymeleafを他のテンプレートに変換する機能を維持しながら、完全かつ正確なレイアウト構造で、ブラウザーによって完全に表示されるページにフラグメントを書き込むことができることです。
th:フラグメントなしのフラグメントへのリンク
マークアップセレクターの力のおかげで、 th:フラグメント属性を使用しないフラグメントを含めることができます。 Thymeleafの知識がなくても、別のアプリケーションから来るマークアップコードである可能性があります。
<div id="copy-section"> © 2011 The Good Thymes Virtual Grocery </div>
上記のスニペットは、CSSセレクターと同様に、id属性で参照するだけで使用できます。
<body> ... <div th:insert="~{footer :: #copy-section}"></div> </body>
th:insertとth:replaceの違い(およびth:include)
th:insertとth:replace(およびth:include、3.0以降は推奨されません)の違いは何ですか?
th:挿入は最も簡単です。親(ホスト)タグの本体として特定のフラグメントを挿入するだけです。
th:replaceは 、親(ホスト)タグを特定のフラグメントで上書きします。
th:includeはth:insertに似ていますが、フラグメントを挿入する代わりに、単にフラグメントのコンテンツを挿入します。
つまり、次のようなHTMLフラグメント:
<footer th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </footer>
親(ホスト)タグに3回含まれています:
<body> ... <div th:insert="footer :: copy"></div> <div th:replace="footer :: copy"></div> <div th:include="footer :: copy"></div> </body>
結果が生成されます:
<body> ... <div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> </div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> <div> © 2011 The Good Thymes Virtual Grocery </div> </body>
8.2パラメータ化可能なフラグメント
テンプレートフラグメントのより機能的なメカニズムを作成するために、 th:フラグメントで定義されたフラグメントは、一連のパラメーターを取ることができます。
<div th:fragment="frag (onevar,twovar)"> <p th:text="${onevar} + ' - ' + ${twovar}">...</p> </div>
これには、これらの2つの構文のいずれかを使用して、 th:insertまたはth:replaceからフラグメントを呼び出す必要があります。
<div th:replace="::frag (${value1},${value2})">...</div> <div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>
後者の場合、順序は重要ではないことに注意してください。
<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>
フラグメント引数のないフラグメントローカル変数
フラグメントが引数なしで定義されている場合でも:
<div th:fragment="frag"> ... </div>
上記の2番目の構文を使用してそれらを呼び出すことができます(2番目のみ)。
<div th:replace="::frag (onevar=${value1},twovar=${value2})">
これは、 th:replaceとth:を組み合わせることと同等です:
<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">
フラグメントのローカル変数のこの指定(引数があるかどうか)は、実行前にコンテキストを空にしないことに注意してください。 スニペットは、呼び出しテンプレートで使用される各コンテキスト変数に引き続きアクセスできます。
th:テンプレート内のクレームに対してアサート
th:assert属性は、コンマで区切られた式のリストを示すことができます。コンマで区切ると、評価ごとにtrueとして評価および表示され、そうでない場合は例外がスローされます。
<div th:assert="${onevar},(${twovar} != 43)">...</div>
これは、パラメーターがフラグメント署名と一致することを確認するのに役立ちます。
<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>
8.3柔軟なレイアウト:フラグメントの単純な挿入に加えて
フラグメントのおかげで、テキスト、数値、Beanではなく、マークアップフラグメントであるパラメーターを指定できます。
これにより、テンプレートの呼び出しからのマークアップで強化できるようにフラグメントを作成でき、非常に柔軟なテンプレートレイアウトメカニズムにつながります。
次のスニペットでヘッダー変数とリンク変数を使用していることに注意してください。
<head th:fragment="common_header(title,links)"> <title th:replace="${title}">The awesome application</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}"> <link rel="shortcut icon" th:href="@{/images/favicon.ico}"> <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script> <!--/* Per-page placeholder for additional links */--> <th:block th:replace="${links}" /> </head>
このスニペットは次のように呼び出すことができます。
<head th:replace="base :: common_header(~{::title},~{::link})"> <title>Awesome - Main</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </head>
...そして、結果は、ヘッダーとリンク変数の値として呼び出しテンプレートからの実際の<title>および<link&gtタグの使用になります。その結果、挿入時にフラグメントが構成されます。
<head> <title>Awesome - Main</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> <link rel="stylesheet" href="/awe/css/bootstrap.min.css"> <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css"> </head>
空のフラグメントを使用する
特別なフラグメント式 " empty fragment " (〜{})を使用して、マークアップを示すことができます。 前の例を使用します。
<head th:replace="base :: common_header(~{::title},~{})"> <title>Awesome - Main</title> </head>
フラグメント(リンク)の2番目のパラメーターが空のフラグメントに設定されているため、ブロック<th:block th:replace = "$ {links}" />には何も書き込まれないことに注意してください。
<head> <title>Awesome - Main</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> </head>
非操作トークンを使用する
非操作トークンは、フラグメントに現在のマークアップをデフォルト値として使用する場合のみ、フラグメントのパラメーターとして使用することもできます。 繰り返しますが、common_headerの例を使用します。
<head th:replace="base :: common_header(_,~{::link})"> <title>Awesome - Main</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </head>
title引数(common_headerフラグメントの最初の引数)がno-op (_)に設定される方法を確認します。これは、フラグメントのこの部分がまったく実行されないことを意味します(title = no-operation):
<title th:replace="${title}">The awesome application</title>
結果は次のとおりです。
<head> <title>The awesome application</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> <link rel="stylesheet" href="/awe/css/bootstrap.min.css"> <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css"> </head>
高度なフラグメント挿入条件
" emtpy "フラグメントと " no-operation "フラグメントの両方が利用できるため、非常にシンプルでエレガントな方法で条件付きでフラグメントを挿入できます。
たとえば、ユーザーが管理者である場合にのみ「common :: adminhead」フラグメントを挿入し、そうでない場合は何も挿入しない(emtpyフラグメント)ために、これを実行できます。
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
さらに、「 no-operation 」トークンを使用して、指定された条件が満たされた場合にのみフラグメントを挿入できますが、条件が満たされない場合はマークアップを変更せずに残します。
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _"> Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support. </div>
さらに、 checkExistenceフラグを使用してテンプレートリソースをチェックするようにテンプレートリゾルバーを構成した場合 、フラグメント自体の存在をデフォルト操作の条件として使用できます。
<!-- The body of the <div> will be used if the "common :: salutation" fragment --> <!-- does not exist (or is empty) --> <div th:insert="~{common :: salutation} ?: _"> Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support. </div>
8.4テンプレートフラグメントの削除
サンプルアプリケーションに戻って、製品リストテンプレートの最新バージョンを修正しましょう。
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> </table>
このコードは単なるテンプレートとしては優れていますが、静的なページとして(Thymeleaf処理を行わずにブラウザーで直接開かれた場合)良いプロトタイプにはなりません。
なんで? ブラウザーでは完全に表示されますが、このテーブルには行のみがあり、この行にはデータレイアウトがあるためです。 プロトタイプとしては、現実的には見えません。複数の製品が必要です。より多くのラインが必要です。
それでは、いくつか追加しましょう。
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table>
さて、今は3つありますが、プロトタイプには間違いなく優れています。 しかし...このテンプレートをThymeleafで処理するとどうなりますか?:
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr> <td>Fresh Sweet Basil</td> <td>4.99</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Italian Tomato</td> <td>1.25</td> <td>no</td> <td> <span>2</span> comment/s <a href="/gtvg/product/comments?prodId=2">view</a> </td> </tr> <tr> <td>Yellow Bell Pepper</td> <td>2.50</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Old Cheddar</td> <td>18.75</td> <td>yes</td> <td> <span>1</span> comment/s <a href="/gtvg/product/comments?prodId=4">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table>
最後の2行は固定された行です! もちろん、同じものです。反復は最前列にのみ適用されるため、Thymeleafが他の2つを削除する必要はありません。
テンプレートの処理中にこれらの2行を削除する方法が必要です。 2番目と3番目の<tr>タグでth:remove属性を使用してみましょう。
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd" th:remove="all"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr th:remove="all"> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table>
処理後、すべてが次のようになります。
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr> <td>Fresh Sweet Basil</td> <td>4.99</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Italian Tomato</td> <td>1.25</td> <td>no</td> <td> <span>2</span> comment/s <a href="/gtvg/product/comments?prodId=2">view</a> </td> </tr> <tr> <td>Yellow Bell Pepper</td> <td>2.50</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Old Cheddar</td> <td>18.75</td> <td>yes</td> <td> <span>1</span> comment/s <a href="/gtvg/product/comments?prodId=4">view</a> </td> </tr> </table>
そして、これらの属性はすべてどういう意味ですか? th:値は、 removeの動作が異なる場合があります。
all :タグを含むタグとそのすべての子の両方を削除します。
body :含まれるタグを削除しないで、そのすべての子を削除します。
tag :含まれるタグを削除しますが、その子は削除しません。
all-but-first :含まれるタグの最初を除くすべての子を削除します。
none :何もしません。 この値は、動的評価に役立ちます。
all-but-firstを使用すると便利なのはいつですか? これにより、プロトタイプを作成するときにremove = "all"を保存できます。
<table> <thead> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> </thead> <tbody th:remove="all-but-first"> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </tbody> </table>
th:remove属性は、有効な文字列値(all、tag、body、all-but-firstまたはnone)のいずれかを返す場合、標準のThymeleaf式を受け入れることができます。
これは、たとえば次のように、削除を条件付きにすることができることを意味します。
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
また、 th:removeはnullを noneの同義語と見なすため、次の例は上記の例と同じように機能します。
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>
この場合、 $ {condition}が falseの場合、 nullが返されるため、削除は実行されません。