[Thymeleaf + Spring] 8. Template Layout
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#template-layout
Tutorial: Using Thymeleaf
1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a
www.thymeleaf.org
틀린 해석이나 잘못된 내용은 알려주시면 감사합니다 😇
8.1 Including template fragments
Defining and referencing fragments
템플릿에 중복되는 부분을 footers, headers, menus 등으로 묶고 싶을 때가 있다.
Thymeleaf needs us to define these parts, “fragments”, for inclusion, which can be done using the th:fragment attribute.
이때 fragment 를 사용한다.
앱개발자였던 나는 fragment 자체에 되게 친숙한 단어이다 onCreateView....,
굳이 이걸 또 조각내서 이걸 따로 붙여야해? 라고 생각할 수 있지만
공통되는 부분은 언제나... 늘... 바뀌기 마련이다...ㅎㅎ......
변경 시에 엄청난 힘을 발휘하기 때문에 어느정도 코드 작성이 끝나고 나면 중복되는 부분을 묶어서 정리해두면 좋다
웹으로 돌아오고 html 을 짜면서 xml 레이아웃 파일에 적응되어있던 나는
늘 헷갈리는게 첫 기본 틀이다
그치만 타임리프는 아주 잘 설명해주고 있다
예제) /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>
이 footer 파일을 쓰려면 th:insert 나 th:replace를 사용한다
보면 div 에 copy라는 태그로 fragment를 묶어주었는데 이렇게 정의 된 코드를 실제로 사용하려면
th:insert="~{어디파일의 :: fragment 이름}"
으로 사용하면 된다
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
Fragment specification syntax
근데 진짜 설명 + 예제 잘 되어있는 듯
The syntax of fragment expressions is quite straightforward. There are three different formats:
- "~{templatename::selector}" Includes the fragment resulting from applying the specified Markup Selector on the template named templatename. Note that selector can be a mere fragment name, so you could specify something as simple as ~{templatename::fragmentname} like in the ~{footer :: copy} above.
- Markup Selector syntax is defined by the underlying AttoParser parsing library, and is similar to XPath expressions or CSS selectors. See Appendix C for more info.
- "~{templatename}" Includes the complete template named templatename.
- Note that the template name you use in th:insert/th:replace tags will have to be resolvable by the Template Resolver currently being used by the Template Engine.
- ~{::selector}" or "~{this::selector}" Inserts a fragment from the same template, matching selector. If not found on the template where the expression appears, the stack of template calls (insertions) is traversed towards the originally processed template (the root), until selector matches at some level.
1. 템플릿 부분에서 일부만 사용하고 싶다 "~{templatename :: fragmentname}"
2. 전체를 사용하고 싶다 "~{templatename}
3. 조건에 따라 다르게 넣고 싶다
<div th:insert="~{ footer :: (${조건1}? #{footer.원하는fragmentname} : #{footer.조건아닐때쓸fragmentname})}"></div>
<div th:insert="~{ footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser}) }"></div>
Fragments can include any th:* attributes.
Referencing fragments without th:fragment
...
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
...
id로 설정된 부분도 가져올 수 있다
<body>
...
<div th:insert="~{footer :: #copy-section}"></div>
</body>
Difference between th:insert and th:replace
th:insert 는 그 specified fragment 만 본문에 넣어줌
th:replace 는 태그 자체를 그 specified fragment으로 바꿔줌
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
<body>
...
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
이렇게 해주면
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
<div> 안에 내용을 넣어줄건지 -> insert
<div> 를 내용으로 바꿀 건지 -> replace
8.2 Parameterizable fragment signatures
In order to create a more function-like mechanism for template fragments, fragments defined with th:fragment can specify a set of parameters:
변수를 받아서 뿌려줄 수도 있다.
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
실제 사용할 때는 변수에 넣을 값을 포함한다
<div th:replace="~{ ::frag (${value1},${value2}) }">...</div>
<div th:replace="~{ ::frag (onevar=${value1},twovar=${value2}) }">...</div>
두번째 방식으로 하면 순서가 달라도 된다
Fragment local variables without fragment arguments
선언 시에 변수가 없는 fragment 인 경우에도
<div th:fragment="frag">
...
</div>
이렇게 값을 던져줄 수 있다
<div th:replace="~{::frag (onevar=${value1},twovar=${value2})}">
<div th:replace="~{::frag}" th:with="onevar=${value1},twovar=${value2}">
this specification of local variables for a fragment – no matter whether it has an argument signature or not – does not cause the context to be emptied prior to its execution. Fragments will still be able to access every context variable being used at the calling template like they currently are.
th:assert for in-template assertions
The th:assert attribute can specify a comma-separated list of expressions which should be evaluated and produce true for every evaluation, raising an exception if not.
<div th:assert="${onevar},(${twovar} != 43)">...</div>
받는 변수에서 assert 를 사용할 수 있고
<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>
만약 assert 가 false 이면 예외를 발생시켜준다
8.3 Flexible layouts: beyond mere fragment insertion
specify parameters 를 지정해줄 수 있기 때문에, 매우 유연하게 템플릿 레이아웃을 사용할 수 있다
타이틀이 각 페이지마다 다를 경우에 이렇게 하면 하나의 파일로 다 사용할 수 있다
<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>
...
replace를 사용했기 때문에 아예 head 자체를 바꿔치기할 수 있다
...
<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>
...
Using the empty fragment
사용하다보면 변수가 들어갈 게 없을 수도 있다 이때는 ~{} << 이 값을 던져준다
<head th:replace="~{ base :: common_header(~{::title},~{}) }">
<title>Awesome - Main</title>
</head>
...
위의 예제와 달리 stylesheet 링크를 뺄 수 있다
...
<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>
...
Using the no-operation token
기본값을 지정해 두고 no-op 을 매개변수로 사용할 수 있다
...
<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>
...
이렇게 _ << 로 표시된 곳이 있으면 그냥 변수 안받고 fragment 기본 값으로 사용하겠다는 뜻이 된다
title 이 _ 로 되어있으니 title 부분을 잘 보면
원래 있던 fragment 의 텍스트는 The awesome application 이고
fragment를 no_op 으로 사용하려는 위의 예제는 Awesome - Main 이다
여기서 replace 를 해주면
<title th:replace="${title}">The awesome application</title>
title 내용이 원래 fragment 에 임시(? 라기 보다는 기본 설정) 제목으로 들어와있고 나머지만 변경 된 것을 볼 수 있다
...
<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>
...
Advanced conditional insertion of fragments
조건에 따라 fragment 를 유연하게 사용하는 방법이다
관리자만 adminhead 를 넣고 아닌 사람은 빈값 ~() 처리
...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
...
~{} 일 경우에는 그대로 두겠다고 할 수도 있다
...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...
Additionally, if we have configured our template resolvers to check for existence of the template resources –- by means of their checkExistence flag -– we can use the existence of the fragment itself as the condition in a default operation:
...
<!-- 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 Removing template fragments
퍼블리싱 또는 디자인을 위해 가짜로 넣어둔 데이터가 있을 수 있다
그리고 데이터를 받아오기 전에 html 파일만 열었을 때도 어떤 형태인지 알아보려면 모의 데이터가 충분히 있어야한다
보통 디자인이나 레이아웃을 받으면 임시 데이터를 불러오는데
<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>
"odd" 라고 임시로 만들어준 데이터들이 있다 이를 제거하기 위해서 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>
And what does that all value in the attribute, mean? th:remove can behave in five different ways, depending on its value:
- all: Remove both the containing tag and all its children. 적용 태그부터 하위 태그 모두 제거
- body: Do not remove the containing tag, but remove all its children. 하위 태그만 모두 제거
- tag: Remove the containing tag, but do not remove its children. 적용태그는 제거하는데 하위는 제거x
- all-but-first: Remove all children of the containing tag except the first one. 첫번째 태그를 제외하고 모든 하위 항목 제거
- none : Do nothing. This value is useful for dynamic evaluation.
What can that all-but-first value be useful for? It will let us save some th:remove="all" when prototyping:
보면 실제 사용할 할 th 태그에 all-but-first 가 적용되어있다
그러면 each 문이 동작하는 실 데이터를 가져오는 태그는 그대로 있고
하단의 모의 데이터만 날릴 수 있다
<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>
The th:remove attribute can take any Thymeleaf Standard Expression, as long as it returns one of the allowed String values (all, tag, body, all-but-first or none).
This means removals could be conditional, like:
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
Also note that th:remove considers null a synonym to none, so the following works the same as the example above:
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>
In this case, if ${condition} is false, null will be returned, and thus no removal will be performed.
8.5 Layout Inheritance
단일 파일 자체를 레이아웃으로 사용할 수도 있다
전체를 fragment로 두고 데이터를 받아온다
<!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>
<!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>