Как стать автором
Обновить

Учебник Thymeleaf: Глава 8. Макет шаблона

Время на прочтение13 мин
Количество просмотров38K
Оглавление

8 Макет шаблона / Template Layout


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>

Синтаксис фрагмента


Синтаксис фрагментных выражений довольно прост. Существует три разных формата:

  • ~{templatename::selector} Включает фрагмент, полученный в результате применения указанного «Селектора разметки» (Markup Selector) в шаблоне с именем templatename. Обратите внимание, что селектор может быть простым именем фрагмента, поэтому вы можете указать что-то простое, как ~{templatename::fragmentname} как в ~{footer :: copy} выше.

    Синтаксис селектора разметки определяется базовой анализирующей библиотекой AttoParser и аналогичен выражениям XPath или селекторам CSS. См. Приложение C для получения дополнительной информации.
  • ~{templatename} Включает полный шаблон с именем templatename. Обратите внимание, что имя шаблона, которое вы используете в тегах th:insert/th:replace должно быть разрешено с помощью Resolver Template, который в настоящее время используется Template Engine.
  • ~{::selector} или ~{this::selector} Вставляет фрагмент из того же шаблона, соответствующий селектору. Если он не найден в шаблоне, где отображается выражение, стек шаблонных вызовов (вставок) перемещается в направлении первоначально обработанного шаблона (в корень), пока селектор не будет соответствовать на некотором уровне. Оба templatename и селектор в приведенных выше примерах могут быть полнофункциональными выражениями (даже условными!), Такими как:

    <div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>


Запомните еще раз, блок ~{...} является необязательным в th:insert/th:replace.

Фрагменты могут включать любые th:* атрибуты. Эти атрибуты будут выполняться после того, как фрагмент будет включен в целевой шаблон (тот, который содержит атрибут th:insert/th:replace и они смогут ссылаться на любые контекстные переменные, определенные в этом целевом шаблоне.

Большое преимущество этого подхода к фрагментам заключается в том, что вы можете писать свои фрагменты на страницах, которые отлично отображаются браузером, с полной и даже правильной структурой разметки, сохраняя при этом возможность превращать Thymeleaf в другие шаблоны.

Ссылка на фрагменты без th:fragment

Благодаря силе «Селектора разметки» (Markup Selectors), мы можем включать фрагменты, которые не используют атрибуты th:fragment. Это может быть даже код разметки, поступающий из другого приложения без знания Thymeleaf:

<div id="copy-section">
  © 2011 The Good Thymes Virtual Grocery
</div>

Мы можем использовать фрагмент выше, просто ссылаясь на него по его атрибуту id, аналогично селектору CSS:

<body>
  ...
  <div th:insert="~{footer :: #copy-section}"></div>
</body>

Разница между th:insert и th:replace (и th:include)

И какая разница между th:insert и th:replace (и th:include, не рекомендуется с 3.0)?

th:insert простейший: он просто вставляет определенный фрагмент как тело своего родительского (host) тега.

th:replace перезаписывает свое родительский (host) тег с определенным фрагментом.

th:include похож на th:insert, но вместо вставки фрагмента он просто вставляет содержимое фрагмента.

То есть такой HTML фрагмент:

<footer th:fragment="copy">
  © 2011 The Good Thymes Virtual Grocery
</footer>

включенный три раза в родительский (host) тег :

<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:fragment, могут принимать набор параметров:

<div th:fragment="frag (onevar,twovar)">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>

Для этого требуется использовать один из этих двух синтаксисов для вызова фрагмента из 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>

мы могли бы использовать второй синтаксис, указанный выше, для их вызова (и только второй):

<div th:replace="::frag (onevar=${value1},twovar=${value2})">

Это будет эквивалентно сочетанию th:replace и th:with:

<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">

Обратите внимание, что эта спецификация локальных переменных для фрагмента — независимо от того, имеет ли она аргументы или нет — не приводит к опустошению контекста до его выполнения. Фрагменты будут по-прежнему иметь доступ к каждой переменной контекста, используемой в вызывающем шаблоне.

th:assert для утверждений в шаблоне

Атрибут th:assert может указывать список выражений, разделенных запятыми, которые должны оцениваться и отображаться как истинные для каждой оценки, и выбрасывать исключения, в противном случае.

<div th:assert="${onevar},(${twovar} != 43)">...</div>

Это полезно для проверки соответствия параметров сигнатуре фрагмента:

<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>

8.3 Гибкие макеты: помимо простой вставки фрагментов


Благодаря фрагментам мы можем указать параметры, которые не являются текстами, цифрами, beans… но вместо этого являются фрагментами разметки.

Это позволяет нам создавать фрагменты таким образом, чтобы их можно было обогатить разметкой, исходящей из вызывающих шаблонов, что приводит к очень гибкому механизму компоновки шаблонов.

Обратите внимание на использование переменных заголовка и ссылок в следующем фрагменте:

<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>

Использование пустого фрагмента

Специальное фрагментное выражение "пустой фрагмент" (~{}), может использоваться для указания разметки. Используя предыдущий пример:

<head th:replace="base :: common_header(~{::title},~{})">
  <title>Awesome - Main</title>
</head>

Обратите внимание, как второй параметр фрагмента (ссылки) установлен на пустой фрагмент, и поэтому ничего не записывается для блока <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>

Кроме того, если мы настроили наши resolver'ы шаблонов для проверки наличия ресурсов шаблона — с помощью флага 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>

Ok, сейчас мы имеем три, определенно лучше для прототипа. Но… что случится при обработки этого шаблона с 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>

Последние две строки — замороженные строки! Ну, конечно, те самые: итерация применяется только к первому ряду, поэтому нет причин, по которым Thymeleaf должен был удалить две другие.

Нам нужен способ удалить эти две строки во время обработки шаблонов. Давайте используем атрибут th:remove во втором и третьем тегах <tr>:

<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? Это позволит нам сохранить некоторые th: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 может принимать любое стандартное выражение Thymeleaf, если оно возвращает одно из допустимых значений String (all, tag, body, all-but-first или none).

Это означает, что удаление может быть условным, например:

<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} ложно, null будет возвращен, и поэтому удаление не будет выполнено.
Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии0

Публикации

Истории

Работа

Java разработчик
367 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань