欢迎您的访问
专注于分享最有价值的互联网技术干货

八、模板布局

几个T的资料等你来白嫖
双倍快乐

八、模板布局

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:insertth:replace属性之一以及th:include轻松地将其包含在我们的主页中,尽管自 Thymeleaf 3.0 起不再建议使用它:

<body>

  ...

  <div th:insert="~{footer :: copy}"></div>

</body>

请注意,th:insert期望一个 fragment 表达式(~{...}),它是一个导致片段的表达式。但是,在上面的示例中,它是一个非复杂的 fragment 表达式\,(~{})包围是完全可选的,因此上述代码等效于:

<body>

  ...

  <div th:insert="footer :: copy"></div>

</body>

片段规范语法

片段表达式的语法非常简单。有三种不同的格式:

  • "~{templatename::selector}"包括将指定的标记 selectors 应用于名为templatename的模板所产生的片段。请注意,selector只能是片段名称,因此您可以指定与~{templatename::fragmentname}一样简单的名称,例如上面的~{footer :: copy}

标记选择器语法由底层 AttoParser 解析库定义,类似于XPath表达式或CSS选择器。有关详细信息,请参阅附录C.

  • "~{templatename}"包括名为templatename的完整模板。

请注意,您在th:insert/th:replace标记中使用的模板名称必须由模板引擎当前正在使用的模板解析器解析。

  • ~{::selector}""~{this::selector}"从同一模板插入与selector匹配的片段。如果在出现表达式的模板上未找到,则将模板调用(插入)堆栈遍历到原始处理的模板( root ),直到selector在某个级别上匹配。

上面示例中的templatenameselector都可以是功能齐全的表达式(甚至是条件表达式!),例如:

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

再次注意周围的~{...}信封在th:insert/th:replace中是可选的。

片段可以包含任何th:*属性。一旦将片段包含到目标模板(具有th:insert/th:replace属性的片段)中,将评估这些属性,并且它们将能够引用此目标模板中定义的任何上下文变量。

这种片段处理方法的一大优势是,您可以将片段写在浏览器可以完美显示的页面中,并具有完整甚至有效的标记结构,同时仍保留使 Thymeleaf 将其包含在其他模板中的功能。

引用没有 th:fragment 的片段

借助标记 selectors 的功能,我们可以包含不使用任何th:fragment属性的片段。甚至可能是完全不了解 Thymeleaf 的来自不同应用程序的标记代码:

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

我们可以使用上面的片段,只需通过其id属性引用它即可,类似于 CSSselectors:

<body>

  ...

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

</body>

th:insert 和 th:replace(和 th:include)之间的区别

th:insertth:replace(和th:include,从 3.0 开始不推荐使用)有什么区别?

  • th:insert最简单:它将简单地将指定的片段作为其 host 标签的主体插入。
  • th:replace实际上将其主机标签替换为指定的片段。
  • th:includeth:insert类似,但是不插入片段,而是仅插入该片段的 content

因此,HTML 片段如下所示:

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

…在主机<div>标签中包含了 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:fragment定义的片段可以指定一组参数:

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

这需要使用以下两种语法之一来从th:insertth:replace调用片段:

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

请注意,在最后一个选项中 Sequences 并不重要:

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

片段局部变量,不带片段参数

即使片段没有这样的参数定义:

<div th:fragment="frag">
    ...
</div>

我们可以使用上面指定的第二种语法来调用它们(并且只有第二种):

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

这等效于th:replaceth:with的组合:

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

请注意 ,对于片段的这种局部变量规范-不管它是否具有参数签名-都不会导致上下文在执行之前被清空。片段仍将能够像当前一样访问调用模板中使用的每个上下文变量。

th:声明模板内 assert

th:assert属性可以指定一个逗号分隔的表达式列表,应对其进行评估,并为每次评估生成 true,否则将引发异常。

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

这对于验证片段签名中的参数非常有用:

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

8.3 灵活的布局:不仅仅是插入片段

由于有了片段表达式,我们可以为片段指定参数,这些参数不是文本,数字,bean 对象……而是标记片段。

这使我们能够以某种方式创建片段,从而可以通过调用模板中的标记来“丰富”片段,从而产生非常灵活的 模板布局机制

请注意以下片段中titlelinks变量的使用:

<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>标记作为titlelinks变量的值,从而导致在插入过程中对片段进行自定义:

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

请注意如何将片段的第二个参数(links)设置为空片段,因此对于<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>
...

使用The No-Operation token

如果我们只想让我们的片段使用其当前标记作为默认值,那么 no-op 也可以用作片段的参数。同样,使用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>
...

有条件的片段高级插入

“空片段”和“The No-Operation token”都可以使用,这使我们能够以非常容易和优雅的方式有条件地插入片段。

例如,我们可以这样做,以便在用户是 Management 员的情况下仅插入common :: adminhead片段(仅*),如果不是,则不插入任何内容(空片段):

...
<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标志检查模板资源是否存在,我们可以使用片段本身的存在作为* default *操作中的条件:

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

好的,现在我们有了三个,对于原型来说绝对更好。但是……当我们用 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 应该删除其他两行。

我们需要一种在模板处理期间删除这两行的方法。让我们在第二个和第三个<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>

该属性中的all值是什么意思? 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 值之一(alltagbodyall-but-firstnone)即可。

这意味着删除可能是有条件的,例如:

<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>

另请注意,th:removenull视为none的同义词,因此以下内容与上面的示例相同:

<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>

在这种情况下,如果${condition}为 false,则将返回null,因此将不执行删除操作。

8.5 布局继承

为了能够将单个文件作为布局,可以使用片段。使用th:fragmentth:replace具有titlecontent的简单布局的示例:

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:replace="${title}">Layout Title</title>
</head>
<body>
    <h1>Layout H1</h1>
    <div th:replace="${content}">
        <p>Layout content</p>
    </div>
    <footer>
        Layout footer
    </footer>
</body>
</html>

此示例声明一个名为 layout 的片段,其中带有 title 和 content 作为参数。在下面的示例中,这两者都将在页面上被继承的片段表达式所替代。

<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}">
<head>
    <title>Page Title</title>
</head>
<body>
<section>
    <p>Page content</p>
    <div>Included on page</div>
</section>
</body>
</html>

在此文件中,html标签将由 layout 替换,但是在布局titlecontent中将分别由titlesection块替换。

如果需要,布局可以由几个片段组成,例如 header 和 footer

赞(0) 打赏
版权归原创作者所有,任何形式转载请联系我们:大白菜博客 » 八、模板布局

评论 抢沙发

8 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏