kagamihogeの日記

kagamihogeの日記です。

spring bootとthymeleafでfragmentsを使用してajaxをする

準備

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <java.version>10.0</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies>

コード

controller

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class FragmentAjaxSampleController {
    @GetMapping("/fragsample")
    public ModelAndView index(ModelAndView model) {
        model.addObject("now", LocalDateTime.now().toString() + " init");
        model.setViewName("fragmentsajax/sample");
        return model;
    }
    
    @GetMapping("/fragsample/frag")
    public String frag(Model model) {
        model.addAttribute("now", LocalDateTime.now().toString() + " frag");
        return "fragmentsajax/sample :: sample-fragment";
    }
    
    @GetMapping("/fragsample/frag-with-selector")
    public String frag002(Model model) {
        model.addAttribute("now", LocalDateTime.now().toString() + " frag-with-selector");
        return "fragmentsajax/sample :: sample-fragment-with-selector//div[id='frag2-sub']";
    }
}

html

src/main/resources/templates/fragmentsajax/sample.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>fragments ajax sample</title>
</head>
<body>
    <div>
        <a href="#" onclick="frag001Click();">ajax fragments</a><br />
        <a href="#" onclick="frag002Click();">ajax fragments-with-selector</a><br />
    </div>
    <div id="frag1" th:fragment="sample-fragment">
        <span th:text="${now}"></span>
    </div>
    
    <div id="frag2" th:fragment="sample-fragment-with-selector">
        <span th:text="${now}"></span> non ajax
        <div id="frag2-sub" class="class1">
            <span th:text="${now}"></span>
        </div>
    </div>
    
    <br />
</body>
<script type="text/javascript">
function frag001Click() {
   var frag1 = document.querySelector('div#frag1');
   
   fetch("/fragsample/frag", {
       method: "get"
   }).then(response => {
       return response.text();
   }).then(body => {
       frag1.outerHTML = body;
   });
}

function frag002Click() {
   var frag2sub = document.querySelector('div#frag2-sub');
   
   fetch("/fragsample/frag-with-selector", {
       method: "get"
   }).then(response => {
       return response.text();
   }).then(body => {
       frag2sub.outerHTML = body;
   });
}
</script>
</html>

説明

fragments + ajax

まず/fragsampleにアクセスすると上記のhtmlが表示される。

次にfrag001Click()をすると/fragsample/fragのコントローラーメソッドが呼ばれ、fragmentsajax/sample :: sample-fragmentを返している。fragmentsajax/sampleは上記のsample.htmlを指しており、その次の:: sample-fragmentはそのhtmlファイル内のth:fragment="sample-fragment"を指している。つまり<div id="frag1" ...の箇所だけを使ってレンダリングせよ、という意味になる。そのため/fragsample/fragにブラウザでアクセスすると次のようなhtmlの断片が返ってくる。

<div id="frag1">
        <span>2018-07-07T15:13:14.685116700 frag</span>
    </div>

最後に、このhtmlの断片をJavaScriptで適当に処理する。

fragments + dom selector + ajax

また、dom selectorでfragments内の要素を指定することもできる。

frag002Click();をすると対象のコントローラーメソッドはfragmentsajax/sample :: sample-fragment-with-selector//div[id='frag2-sub']を返している。これはsample-fragment-with-selectorというfragment内のdivタグでidがfrag2-subのもの、という意味になる。つまり<div id="frag2-sub" ...の箇所だけを使う、という意味になる。そのため/fragsample/frag-with-selectorにブラウザでアクセスすると次のようなhtmlの断片が返ってくる。

<div id="frag2-sub" class="class1">
            <span>2018-07-07T15:18:39.535689500 frag-with-selector</span>
        </div>

fragmentsの外部化

fragmentsの使用例で良く見かけるのはヘッダー・フッターなど共通部分を外部ファイル化することだが、この外部化したfragmentsを使用することもできる。使用例は省略。

<div id="frag001" th:insert="~{footer :: frag001}" class="hoge"></div>

ただし、複数画面に共通では無い、ある画面のajaxでしか使用しないfragmentsを外部化しても見通しが悪くなるだけだと思われる。

感想

この機能使ってるのを初めて見たときちょっと混乱した。というのも、ajaxだからjsonxmlを返してJavaScriptで云々してるだろう、という思い込みがあったためである。とはいえ、個人的には、一度理解してしまえばspringとthymeleafの世界に寄せられるのでまぁまぁ便利な機能ではないか、という感触がある。

参考リンク