# 01.Thymeleaf

## Thymeleaf

## 기본 기능

*   사용 선언

    ```html
    <html xmlns:th="http://www.thymeleaf.org"></html>
    ```
*   속성 변경

    ```html
    th:href="@{/css/bootstrap.min.css}"
    ```
*   URL 링크 표현식

    ```html
    th:href="@{/css/bootstrap.min.css}"
    <!-- -->
    th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
    th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
    th:href="@{|/basic/items/${item.id}|}"
    ```
*   속성 변경

    ```html
    th:onclick="|location.href='@{/basic/items/add}'|"
    ```
*   반복 출력

    ```html
    <tr th:each="item : ${items}"></tr>
    ```
*   변수 표현식

    ```html
    <td th:text="${item.price}">10000</td>
    ```
*   속성 변경

    ```html
    <input
      type="text"
      id="price"
      name="price"
      value="10000"
      th:value="${item.price}"
    />
    ```

### 기본 표현식

[https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax)

```
ㅇ 간단한 표현
  - 변수 표현식: ${...}
  - 선택 변수 표현식: \*{...}
  - 메시지 표현식: #{...}
  - 링크 URL 표현식: @{...}
  - 조각 표현식: ~{...}

ㅇ 리터럴
  - 텍스트: 'one text', 'Another one!',…
  - 숫자: 0, 34, 3.0, 12.3,…
  - 불린: true, false
  - 널: null
  - 리터럴 토큰: one, sometext, main,…

ㅇ 문자 연산
  - 문자 합치기: +
  - 리터럴 대체: |The name is ${name}|

ㅇ 산술 연산
  - Binary operators: +, -, \*, /, %
  - Minus sign (unary operator): -

ㅇ 불린 연산
  - Binary operators: and, or
  - Boolean negation (unary operator): !, not

ㅇ 비교와 동등
  - 비교: >, <, >=, <= (gt, lt, ge, le)
  - 동등 연산: ==, != (eq, ne)

ㅇ 조건 연산
  - If-then: (if) ? (then)
  - If-then-else: (if) ? (then) : (else)
  - Default: (value) ?: (defaultvalue)

ㅇ 특별한 토큰
  - No-Operation: \_
```

### 텍스트

*   텍스트 출력

    * 기본적으로 escape 를 제공
      * escape : HTML 에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것
      * HTML 엔티티 : `<` 문자를 태그의 시작이 아닌 문자로 표현하는 방법

    ```html
    <ul>
      <li>th:text 사용 = <span th:text="${data}"></span></li>
      <li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>
    </ul>
    ```
*   Unescape

    * `th:text` --> `th:utext`
    * `[[...]]` --> `[(...)]`

    ```html
    <ul>
      <li>th:utext = <span th:utext="${data}"></span></li>
      <li><span th:inline="none">[(...)] = </span>[(${data})]</li>
    </ul>
    ```

### SpringEL 표현식

**Object**

* `${user.username}` = userA
* `${user['username']}` = userA
* `${user.getUsername()}` = userA

**List**

* `${users[0].username}` = userA
* `${users[0]['username']}` = userA
* `${users[0].getUsername()}` = userA

**Map**

* `${userMap['userA'].username}` = userA
* `${userMap['userA']['username']}` = userA
* `${userMap['userA'].getUsername()}` = userA

**Safe Navigation Operator**

```html
<div th:if="${errors?.containsKey('globalError')}"></div>
```

* errors`?.` 은 errors 가 null 일때 NullPointerException 대신, null 을 반환하는 문법 [참고](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-operator-safe-navigation)

**지역변수**

```html
<div th:with="first=${users[0]}">
  <p>first member name : <span th:text="${first.username}"></span></p>
</div>
```

### 기본 객체

**Thymeleaf 기본 객체**

* `${#request}`
* `${#response}`
* `${#session}`
* `${#servletContext}`
* `${#locale}`

**편의 객체**

* HTTP 요청 파라미터 접근: `${param.paramData}`
* HTTP 세션 접근: `${session.sessionData}`
* 스프링 빈 접근: `${@helloBean.hello('Spring!')}`

### 유틸리티 객체와 날짜

* \#message : 메시지, 국제화 처리
* \#uris : URI 이스케이프 지원
* \#dates : java.util.Date 서식 지원
* \#calendars : java.util.Calendar 서식 지원
* \#temporals : 자바8 날짜 서식 지원
* \#numbers : 숫자 서식 지원
* \#strings : 문자 관련 편의 기능
* \#objects : 객체 관련 기능 제공
* \#bools : boolean 관련 기능 제공
* \#arrays : 배열 관련 기능 제공
* \#lists , #sets , #maps : 컬렉션 관련 기능 제공
* \#ids : 아이디 처리 관련 기능 제공, 뒤에서 설명

**Reference**

> [타임리프 유틸리티 객체](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utilityobjects)
>
> [유틸리티 객체 예시](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-b-expressionutility-objects)

### URL 링크

* 단순 URL
  *   /hello

      ```html
      <a th:href="@{/hello}"></a>
      ```
* query parameter
  *   /hello?param1=data1\&param2=data2

      ```html
      <a th:href="@{/hello(param1=${param1}, param2=${param2})}"></a>
      ```
* path variable
  *   /hello/data1/data2

      ```html
      <a
        th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}"
      ></a>
      ```
* query parameter + path variable
  *   /hello/data1?param2=data2

      ```html
      <a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}"></a>
      ```

**Reference**

> [https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#link-urls](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#link-urls)

### 리터럴

*   문자: 'hello' (문자 리터럴은 항상 작은따옴표로 감싸야 함)

    ```html
    <span th:text="'hello'"></span>
    ```
* 숫자: 10
* 불린: true , false
* null: null

### 연산

*   산술 연산

    ```html
    10 + 2 = <span th:text="10 + 2"></span>
    ```
*   비교 연산

    ```html
    <!-- 
      > (gt)
      < (lt)
      >= (ge)
      <= (le)
      ! (not)
      == (eq)
      != (neq, ne)`
    -->
    1 >= 10 = <span th:text="1 >= 10"></span>
    ```
*   조건식

    ```html
    (10 % 2 == 0) ? = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span>
    ```
*   Elvis 연산자

    ```html
    <!-- 데이터가 없을 경우 설정 문자열 출력 -->
    ${data} = <span th:text="${data}?: '데이터가 없습니다.'"></span>
    ```
*   No-Operation

    ```html
    <!-- 데이터가 없을 경우 tag 데이터 그대로 출력 (Thymeleaf 가 실행되지 않는 것 처럼 동작) -->
    ${data} = <span th:text="${data}?: _">데이터가 없습니다.</span>
    ```

### 속성 값 설정

*   속성 설정

    ```html
    <input type="text" name="mock" th:name="userA" />
    ```
*   속성 추가

    ```html
    <input type="text" class="text" th:classappend="large" /><br />
    ```
*   checked 처리

    ```html
    <input type="checkbox" name="active" th:checked="true" /><br />
    <input type="checkbox" name="active" th:checked="false" /><br />
    <input type="checkbox" name="active" th:checked="${isChecked}" /><br />
    ```

### 반복

*   반복

    ```html
    <tr th:each="user : ${users}">
      <td th:text="${user.username}">username</td>
      <td th:text="${user.age}">0</td>
    </tr>
    ```
*   상태 유지

    ```html
    <!-- 생략 시 userStat 로 사용 -->
    <tr th:each="user, state : ${users}"></tr>
    ```

    * index : 0부터 시작
    * count : 1부터 시작
    * size : 전체 사이즈
    * even , odd : 홀/짝수 여부
    * first , last :처음/마지막 여부
    * current : 현재 객체

### 조건부 평가

*   if, unless

    ```html
    <span th:text="'어른'" th:if="${user.age gt 20}"></span>
    <span th:text="'어른'" th:unless="${user.age le 20}"></span>
    ```
*   switch

    ```html
    <td th:switch="${user.age}">
      <span th:case="10">10살</span>
      <span th:case="20">20살</span>
      <span th:case="*">기타</span>
    </td>
    ```

### 주석

*   표준 HTML 주석

    * 타임리프가 렌더링하지 않고 유지

    ```html
    <!-- <span th:text="${data}"></span> -->
    ```
*   타임리프 파서 주석

    * 렌더링에서 주석 부분을 제거 (타임리프 주석)

    ```html
    <!--/* [[${data}]] */-->

    <!--/*-->
    <span th:text="${data}">html data</span>
    <!--*/-->
    ```
*   타임리프 프로토타입 주석

    * 타임리프 렌더링을 거쳐야만 이 부분이 정상 렌더링 (HTML 에서만 주석 처리)

    ```html
    <!--/*/
    <span th:text="${data}">html data</span>
    /*/-->
    ```

### 블록

* th:each 로 해결이 어려울 때 사용

```html
<th:block th:each="user : ${users}">
  <div>
    name: <span th:text="${user.username}"></span> age:
    <span th:text="${user.age}"></span>
  </div>
  <div>요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span></div>
</th:block>
```

### JavaScript Inline

*   javascript inline

    ```html
    <script th:inline="javascript">
      var username = [[${user.username}]];
      var age = [[${user.age}]];
      //자바스크립트 내추럴 템플릿
      var username2 = /*[[${user.username}]]*/ "test username";
      //객체
      var user = [[${user}]];
    </script>

    <!--
        var username = "userA";
        var age = 10;
        var username2 = "userA";
        var user = {"username":"userA","age":10};
    -->
    ```
*   each

    ```html
    <script th:inline="javascript">
      [# th:each="user, stat : ${users}"]
      var user[[${stat.count}]] = [[${user}]];
      [/]
    </script>
    <!--
      var user1 = {"username":"userA","age":10};
      var user2 = {"username":"userB","age":20};
      var user3 = {"username":"userC","age":30};
    -->
    ```

### 템플릿 조각

*   `/resources/templates/template/fragment/footer.html`

    ```html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <body>
        <footer th:fragment="copy">푸터 자리 입니다.</footer>

        <footer th:fragment="copyParam (param1, param2)">
          <p>파라미터 자리 입니다.</p>
          <p th:text="${param1}"></p>
          <p th:text="${param2}"></p>
        </footer>
      </body>
    </html>
    ```
*   `/resources/templates/template/fragment/fragmentMain.html`

    ```html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <head>
        <meta charset="UTF-8" />
        <title>Title</title>
      </head>
      <body>
        <h1>부분 포함</h1>
        <h2>부분 포함 insert (div tag 안에 삽입)</h2>
        <div th:insert="~{template/fragment/footer :: copy}"></div>

        <h2>부분 포함 replace (div tag 대체)</h2>
        <div th:replace="~{template/fragment/footer :: copy}"></div>

        <h1>파라미터 사용</h1>
        <div
          th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"
        ></div>
      </body>
    </html>
    ```

### 템플릿 레이아웃

**동적인 레이아웃**

*   `/resources/templates/template/layout/base.html`

    * 레이아웃이라는 큰 틀

    ```html
    <html xmlns:th="http://www.thymeleaf.org">
      <head th:fragment="common_header(title,links)">
        <title th:replace="${title}">레이아웃 타이틀</title>

        <!-- 공통 -->
        <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>

        <!-- 추가 -->
        <th:block th:replace="${links}" />
      </head>
    </html>
    ```
*   `/resources/templates/template/layout/layoutMain.html`

    * 틀 안에 필요한 코드 조각들을 전달
    * ::title 은 현재 페이지의 title tag 들을 전달
    * ::link 은 현재 페이지의 link tag 들을 전달

    ```html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <head
        th:replace="template/layout/base :: common_header(~{::title},~{::link})"
      >
        <title>메인 타이틀</title>
        <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />
        <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}" />
      </head>
      <body>
        메인 컨텐츠
      </body>
    </html>
    ```

**메인 레이아웃**

*   `/resources/templates/template/layoutExtend/layoutFile.html`

    * 기본 레이아웃(header, footer) 틀은 유지하고 title, content 만 변경

    ```html
    <!DOCTYPE html>
    <html
      th:fragment="layout (title, content)"
      xmlns:th="http://www.thymeleaf.org"
    >
      <head>
        <title th:replace="${title}">레이아웃 타이틀</title>
      </head>
      <body>
        <h1>레이아웃 H1</h1>
        <div th:replace="${content}">
          <p>레이아웃 컨텐츠</p>
        </div>
        <footer>레이아웃 푸터</footer>
      </body>
    </html>
    ```
*   `/resources/templates/template/layoutExtend/layoutExtendMain.html`

    * 기본 레이아웃 틀로 교체하는데 하는데 title, content 는 전달

    ```html
    <!DOCTYPE html>
    <html
      th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}"
      xmlns:th="http://www.thymeleaf.org"
    >
      <head>
        <title>메인 페이지 타이틀</title>
      </head>
      <body>
        <section>
          <p>메인 페이지 컨텐츠</p>
          <div>메인 페이지 포함 내용</div>
        </section>
      </body>
    </html>
    ```

## Form

### 입력 폼 처리

```html
<form action="item.html" th:action th:object="${item}" method="post">
  <div>
    <label for="itemName">상품명</label>
    <input
      type="text"
      id="itemName"
      th:field="*{itemName}"
      class="formcontrol"
      placeholder="이름을 입력하세요"
    />
  </div>
</form>
```

* `th:object` : 커맨드 객체를 지정
* `\*{...}` : 선택 변수 식 (th:object 에서 선택한 객체에 접근)
* `th:field` : HTML 태그의 id , name , value 속성을 자동으로 생성
*   렌더링 전/후

    ```html
    <input type="text" th:field="*{itemName}" />
    ```

    ```html
    <input type="text" id="itemName" name="itemName" th:value="*{itemName}" />
    ```

### 체크 박스

#### 단일

**Register**

```html
<div>
  <div class="form-check">
    <input
      type="checkbox"
      id="open"
      th:field="*{open}"
      class="form-checkinput"
    />
    <label for="open" class="form-check-label">판매 오픈</label>
  </div>
</div>
```

* 타임리프가 자동으로 `<input type="hidden" name="_open" value="on"/>` 생성
  * 체크 박스를 체크할 경우 on 을 전달하지만, 체크하지 않을 경우 값 자체를 전달하지 않음 -> 이 경우 hidden type 의 \_name input 태그를 사용하게 되면, false 로 값을 전달 (\_open=on)

**View**

```html
<div>
  <div class="form-check">
    <input
      type="checkbox"
      id="open"
      th:field="${item.open}"
      class="form-check-input"
      disabled
    />
    <label for="open" class="form-check-label">판매 오픈</label>
  </div>
</div>
```

#### 멀티

(참고) @ModelAttribute 를 사용하면 해당 Controller 호출 시 regions() 에서 반환한 값이 자동으로 Model에 항상 담기게 됨

```java
@ModelAttribute("regions")
public Map<String, String> regions() {
    Map<String, String> regions = new LinkedHashMap<>();
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");
    return regions;
}
```

**Register**

```html
<div th:each="region : ${regions}" class="form-check form-check-inline">
  <input
    type="checkbox"
    th:field="*{regions}"
    th:value="${region.key}"
    class="form-check-input"
  />
  <label
    th:for="${#ids.prev('regions')}"
    th:text="${region.value}"
    class="form-check-label"
    >서울</label
  >
</div>
```

* 타임리프는 each로 체크박스 생성 시 동적으로 id에 순번을 매겨준다. `#ids`는 동적으로 생성된 id를 인식
*   result

    ```html
    <input
      type="checkbox"
      value="SEOUL"
      class="form-check-input"
      id="regions1"
      name="regions"
    />
    <input
      type="checkbox"
      value="BUSAN"
      class="form-check-input"
      id="regions2"
      name="regions"
    />
    <input
      type="checkbox"
      value="JEJU"
      class="form-check-input"
      id="regions3"
      name="regions"
    />
    ```

**View**

```html
<div th:each="region : ${regions}" class="form-check form-check-inline">
  <input
    type="checkbox"
    th:field="${item.regions}"
    th:value="${region.key}"
    class="form-check-input"
    disabled
  />
  <label
    th:for="${#ids.prev('regions')}"
    th:text="${region.value}"
    class="form-check-label"
    >서울</label
  >
</div>
```

* 타임리프는 `th:field`에 지정한 값과 `th:value`의 값을 비교해서 체크를 자동으로 처리

### 라디오 버튼

```java
public enum ItemType {

    BOOK("도서"), FOOD("식품"), ETC("기타"); // NAME(description)

    private final String description;

    ItemType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}
```

```html
<div th:each="type : ${itemTypes}" class="form-check form-check-inline">
  <input
    type="radio"
    th:field="*{itemType}"
    th:value="${type.name()}"
    class="form-check-input"
  />
  <label
    th:for="${#ids.prev('itemType')}"
    th:text="${type.description}"
    class="form-check-label"
  >
    BOOK
  </label>
</div>
```

* 라디오 버튼은 항상 하나의 값이 선택되어야 하므로 히든 버튼을 따로 생성하지 않음.

### 셀렉트 박스

**Register**

```html
<select th:field="*{deliveryCode}" class="form-select">
  <option value="">==배송 방식 선택==</option>
  <option
    th:each="deliveryCode : ${deliveryCodes}"
    th:value="${deliveryCode.code}"
    th:text="${deliveryCode.displayName}"
  >
    FAST
  </option>
</select>
```

## Reference

> [공식 사이트](https://www.thymeleaf.org/)
>
> [공식 메뉴얼 - 기본 기능](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html)
>
> [공식 메뉴얼 - 스프링 통합](https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html)
