【Spring Boot】Thymeleafのフラグメント式 html編

共通の要素やページのヘッダー、フッターは毎回書かずに一箇所で管理して、必要な時に出力できると便利です。今回はhtmlに記載された要素・コンテンツを別の場所に出力する際に使用するThymeleafの“フラグメント式”について紹介します。

開発環境
・Spring Boot:2.6.2
・Thymeleaf:3.0.14.RELEASE
・Java:1.8

フラグメント式について

まずフラグメント式とは何か、そしてどんなメリットがあるのかを説明します。

フラグメント式は単純式のひとつ

フラグメント式はThymeleafで用意されている“単純式(Simple Expressions)”の一つです。

単純式はhtmlの属性に値やオブジェクトを渡す際に使用する式で、フラグメント式の場合は「コピー元の情報」を渡します。コピー元の情報を渡すことで出力したい要素の特定が可能になります。

またフラグメント式の他に、下記の単純式が用意されています。

詳細は各記事に記載しています。

フラグメント式のメリット

フラグメント式は用法用量を守って正しく使うことができれば保守性を高めることができます。

ウェブページのヘッダ等のように同じ要素を使い回している場合、その要素に修正が入った際には、該当箇所の洗い出しと該当箇所分の修正が発生してしまいます。ですがフラグメント式を用いて一か所で管理されている要素をコピーして出力している場合は洗い出しが不要ですし、修正もそのコピー元のhtmlのみになります。

フラグメント式の記載方法

フラグメント式を使用するための記載方法を説明します。

基本

先頭に“~”をつけて“{}”でコピー元の情報を囲みます。

コピー元の情報には「出力したい要素が記載されているテンプレート名」と「その要素を特定する情報」を設定できます。

記載方法は下記のとおりです。

  1. ~{テンプレート名 :: 要素を特定する情報}
  2. ~{テンプレート名}
  3. ~{this :: 要素を特定する情報} または ~{:: 要素を特定する情報}

実際のコードと出力結果を確認してみましょう。

HTML
<!--/* index.html */-->
<div style="color: red"   th:insert="~{copy::#copy-element}"></div>
<div style="color: blue"  th:insert="~{copy}"></div>
<div style="color: green" th:insert="~{this::this-element}"></div>

<div th:fragment="this-element">出力内容3</div>
HTML
<!--/* copy.html */-->
<div id="copy-element">出力内容1
  <div>出力内容1ー1</div>
  <div>出力内容1ー2</div>
</div>
<div>出力内容2</div>

○出力結果

1. の場合、コピー元となるのは指定したテンプレートの要素とその子要素以下全てになります。

例の場合はcopy.htmlのid=copy-elementの要素とその子要素すべてを指定したことになり、赤の出力となります。

2. の場合、コピー元となるのは指定したテンプレート全体です。

例の場合はcopy.html全体を指定したことになるため、青の出力となります。

3. の場合、コピー元となるのは自身のテンプレート内の指定した要素とその子要素以下全てになります。

例の場合は自身のテンプレート(index.html)にあるdivタグを指定したことになり、緑の出力となります。また、もし自身のテンプレート上に見つからなかった場合は、呼び出し元(コピー先)にさかのぼって探します。(詳細は後ほど)

上記の「要素を特定する情報」には「セレクタ」「フラグメント名」が使用可能です。

フラグメント式は要素を特定する情報を記載するため、特定しやすいようindex.htmlの6行目のコピー元を示す記述と一緒に用いることも多いです。

コピー元/出力先をそれぞれ指定する際に、一般的に使用されるThymeleaf独自の属性は以下になります。

指定内容独自属性
出力元fragment
出力先insert、replace、include(非推奨)

フラグメント式はThymeleafで用意されている独自の属性を使用するため、xmlネームスペース(xmlns)を設定する必要があります。

HTML
<!--/* prefixには"th"がよく使用される */-->
<html xmlns:th="http://www.thymeleaf.org" >

(参考) th:fragmentについて

少し脱線しますが、フラグメント名とその周辺知識について説明します。フラグメント名とは独自属性fragmentに設定された値のことを指します。

HTML
<!--/* 記載方法 prefix:fragment="フラグメント名" */-->
<p th:fragment="base"></p>

fragment属性に設定したフラグメント名はフラグメント式の「要素を特定する情報」に指定できます。

またfragment属性には引数を設定でき、その引数にパラメータを渡すことで各要素の属性値やコンテンツを変更することができます。

HTML
<div th:replace="~{::example_1(変更後)}"></div>
<div th:replace="~{::example_2(apple,banana)}"></div>

<!--/* 記載方法 prefix:fragment="フラグメント名(引数名)" */-->
<p th:fragment="example_1(param0)" th:text="${param0}">変更前</p>

<!--/* 記載方法 prefix:fragment="フラグメント名(引数名,...)" */-->
<div th:fragment="example_2(param1,param2)">
  <p th:text="${param1}">りんご</p>
  <p th:text="${param2}">ばなな</p>
</div>

○出力結果

同記事内の「パラメータ渡し」ではフラグメント式の観点から上記内容を記載しています。

th:insert、replace、includeについて

フラグメント式の話に戻ります。

独自属性のinsert、replace、includeにはフラグメント式を設定することができます。(ただし、includeは非推奨)

上記三つの出力結果には下記の違いがあります。

HTML
<!-- insertはこの要素の子要素として挿入(この要素は残る) -->
<div style="color: red;" th:insert="~{::example}"></div>
<br/>
<!--/* replaceはこの要素ごと置換(この要素はなくなる) */-->
<div style="color: blue;" th:replace="~{::example}"></div>
<br/>
<!--/* includeはコンテンツのみ挿入 */-->
<div style="color: green;" th:include="~{::example}"></div>
<br/>

<div style="font-weight: bold;" th:fragment="example">
  いちご
  <div>りんご</div>
  ばなな
</div>

○出力結果

○出力結果(html)

HTML
<div style="color: red;">
  <div style="font-weight: bold;">
    いちご
    <div>りんご</div>
    ばなな
  </div>
</div>
<br>
  
<div style="font-weight: bold;">
  いちご
  <div>りんご</div>
  ばなな
</div>
<br>
  
<div style="color: green;">
  いちご
  <div>りんご</div>
  ばなな
</div>
<br>

<div style="font-weight: bold;">
  いちご
  <div>りんご</div>
  ばなな
</div>

理解しやすいようにhtmlの出力結果を確認してみます。

insert(1~7行目)は出力先のdivタグの子要素としてコピー元の要素が挿入されていることがわかります。そのため出力先のスタイルである赤字で画面に出力されています。

replace(10~14行目)は出力先のdivタグごと置換されるためコピー元の要素のみが残っていることが分かります。そのため出力先のスタイルである青字は画面に出力されていません。

include(17~21行目)は出力先のdivタグの子要素としてコピー元の要素が挿入されていることがわかります。ただコンテンツしか出力しないため、出力元のスタイルである太字が付与されていません。

このようにどの属性を使用するかによって出力が大きく変わるため、そのページにあった属性を選択しましょう。

パラメータ渡し

フラグメント式はコピー元にパラメータを渡すことができます。

パラメータはフラグメント式の末尾に”()”をつけ、その中に渡したいパラメータを記載します。複数パラメータを渡すことも可能で、その場合はカンマ区切りで記載します。(「(参考) th:fragmentについて」参照)

fragment属性に引数が設定されている場合、引数名を明記することにより、引数の順番を無視してパラメータを渡すことができます。

HTML
<div th:replace="~{::example(param2=banana,param1=apple)}"></div>

<div th:fragment="example(param1,param2)">
  <p th:text="${param1}">りんご</p>
  <p th:text="${param2}">ばなな</p>
</div>

○出力結果

またfragment属性に引数が設定されていない場合でもパラメータを渡すことができます。その際には対応する変数名を引数名に指定する必要があります。

HTML
<!--/* テンプレートのみでもパラメータ渡しは可能 */-->
<div th:replace="~{copy(fruit=pineapple)}"></div>
HTML
<!--/* copy.html */-->
<div th:text="${fruit}"></div>

出力先の要素をコピー元に渡す

基本の3. で説明した~{::要素を特定する情報}はテンプレートを指定していないため、現在のテンプレートから呼び出し元(出力先)に渡って指定した要素が見つかるまでさかのぼり探します。呼び出し元の指定した要素をコピー元の要素に追加して出力することができるため、headタグ内のリンク追加などに使用されます。このさかのぼるパターンはコピー元の要素にinsert属性、replace属性がある場合に発生します。

オフィシャルサイトにこの機能を使ってheadタグを更新する例があったので、こちらで動作を確認します。(一部の文字列を変更しています)

出力したいheadタグは下記の通りです。タイトルにはページ固有のタイトルが入り、link、scriptタグは共通のものとそのページ固有ものが記載されるものになります。

HTML
<head>

  <title>フラグメント式</title>

  <!--/* ページの共通リンク */-->
  <link rel="stylesheet" type="text/css" media="all" th:href="/css/common.css">
  <link rel="shortcut icon" th:href="/images/common.ico">
  <script type="text/javascript" th:src="/js/common.js"></script>

  <!--/* ページの固有リンク */-->
  <link rel="stylesheet" th:href="/css/unique1.css">
  <link rel="stylesheet" th:href="/css/unique2.css">

</head>

まず、headタグの共通部分を記載しているコピー元(base.html)は下記のようになっています。

HTML
<!-- base.html -->
<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">共通</title>

  <!--/* 共通要素 */-->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/common.css}">
  <link rel="shortcut icon" th:href="@{/images/common.ico}">
  <script type="text/javascript" th:src="@{/js/common.js}"></script>

  <!--/* 固有要素 */-->
  <th:block th:replace="${links}" />

</head>

2行目にはコピー元であることを示すfragment属性が記載されており、7〜9行目には共通のlinkタグとscriptタグが記載されています。

また4行目と12行目には出力先であることを示すreplace属性が記載されており、その値には同テンプレートのfragment属性に渡されるパラメータが設定されます。これは先程説明したさかのぼるパターンであり、呼び出し元のテンプレートや要素を用いて変更できるようになっているようです。

次に呼び出し元です。呼び出し元のheadタグは下記のようになっています。

HTML
<head th:replace="base :: common_header(~{::title},~{::link})">

  <title>フラグメント式</title>

  <link rel="stylesheet" th:href="@{/css/unique1.css}">
  <link rel="stylesheet" th:href="@{/css/unique2.css}">

</head>

1行目に出力先であることを示すreplace属性が記載されており、その引数に”::title”と”::link”が渡されています。

必要なテンプレートは以上となります。では上記二つのテンプレートがある場合、どのような出力になるのか順を追って確認していきましょう。

まず呼び出し元から二つのパラメータを渡されるため、base.htmlは下記のようになります。4行目と12行目の記述が変わっていますね。

HTML
<!-- base.html パラメータ反映後 -->
<head th:fragment="common_header(title,links)">

  <title th:replace="~{::title}">共通</title>

  <!--/* 共通要素 */-->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/common.css}">
  <link rel="shortcut icon" th:href="@{/images/common.ico}">
  <script type="text/javascript" th:src="@{/js/common.js}"></script>

  <!--/* 固有要素 */-->
  <th:block th:replace="~{::links}" />

</head>

この二つの要素はそれぞれ自身より後に出てくるtitleタグ、linkタグを探しそれらをコピー元とします。探索のイメージはそれぞれ下記の通りです。

HTML
<!-- titleタグの探索開始 (base.html の4行目より下の部分で探索) -->

  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/common.css}">
  <link rel="shortcut icon" th:href="@{/images/common.ico}">
  <script type="text/javascript" th:src="@{/js/common.js}"></script>

  <th:block th:replace="~{::links}" />

</head>

<!--/* 
  th:replace="~{::title}"より下の部分にtitleタグなし
  さらに呼び出し元にさかのぼって探索 (以降、呼び出し元のhtml)
*/-->

<head th:replace="base :: common_header(~{::title},~{::link})">

  <!--/* 下記要素が一致 */-->
  <title>フラグメント式</title>
  
  <link rel="stylesheet" th:href="@{/css/unique1.css}">
  <link rel="stylesheet" th:href="@{/css/unique2.css}">

</head>
HTML
<!-- linkタグの探索 (base.html の12行目より下の部分で探索) -->

</head>

<!--/* 
  th:replace="~{::link}"より下の部分にtitleタグなし
  さらに呼び出し元にさかのぼって探索 (以降、呼び出し元のhtml)
*/-->

<head th:replace="base :: common_header(~{::title},~{::link})">

  <title>フラグメント式</title>
  <!--/* 下記2要素が一致 */-->
  <link rel="stylesheet" th:href="@{/css/unique1.css}">
  <link rel="stylesheet" th:href="@{/css/unique2.css}">

</head>

最終的にreplace属性をもつ要素は見つけた要素を用いて置換され、出力したかったheadタグと同じものになります。実際の出力は下記のとおりです。

HTML
<head>

  <title>フラグメント式</title>
  
  <link rel="stylesheet" type="text/css" media="all" href="/css/common.css">
  <link rel="shortcut icon" href="/images/common.ico">
  <script type="text/javascript" src="/js/common.js"></script>
  
  <link rel="stylesheet" href="/css/unique1.css">
  <link rel="stylesheet" href="/css/unique2.css">

</head>

このようにフラグメント式は呼び出し元のテンプレートまでさかのぼり処理をすることもできます。かなり強力な機能ですが、その一方でどのような出力になるのか把握しづらく、複雑にしすぎると想定外の出力となることも考えられるため多用は避け、効果的な場面で使用するようにしましょう。

まとめ

  • フラグメント式はThymeleafの独自属性replaceやinsertにコピー元の情報を渡す単純式。渡した情報はコピー元となる要素の特定に使用される。
  • 記載方法は大きく分けて3パターンある。
    • ~{テンプレート名 :: 要素を特定する情報}
    • ~{テンプレート名}
    • ~{this :: 要素を特定する情報} (~{:: 要素を特定する情報} でもよい)
  • コピー元にパラメータを渡し、反映したものを出力することも可能。パラメータはフラグメント式の末尾に”()”をつけて渡す。
  • パラメータは複数渡すことが可能。複数渡す場合はカンマ区切りで記載する。
  • コピー元の内容はパラメータ(文字列、数値)のほかに、呼び出し元(出力先)の要素に変更することもできる。

強力な機能であるため、ぜひフラグメント式をマスターしましょう!

コメント

タイトルとURLをコピーしました