EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

Thymeleafでvue.jsを使う

最近のブログネタに登場しているように、現在、SpringBoot + ThymeleafでWebアプリの開発を行っています。 このアプリはログインをして使うサービスで、クローズドなものなので、SEOではなくユーザビリティを重視して作ろうと考えています。

特に、複雑な入力フォームや連動して動的に表示するテキストなどがあり、JavaScriptでページ上のDOM操作を行いたいので、テンプレートエンジンのThymeleafでvue.jsと連携させる方法を考えてみました。

実現したいこと

Thymeleafとvue.jsを連携させたいのですが、まずはThymeleafで入力フォームを作って、vue.jsでフォームの制御を行い、最終的にSpringBootに値を送ることを考えています。

また、vue.jsは手軽に使いたいので、コンポーネントを作るのではなく、インラインでスクリプトとして使っています。

やり方

今回のフォームとして、「申請フォーム」を考えています。 項目としては、

  • 名前
  • 内容
  • 商品明細(リスト)
    • 商品名
    • 金額
    • 個数

となっていて、明細は1行ずつ小計(金額×個数)を表示し、それらを足し合わせた合計をフォームの下に表示します。

まず、商品明細用のJavaBeanを用意します。

public class ProductBean {
    private String name;
    private String count;
    private String price;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getCount() {
        return count;
    }
    public void setCount(String count) {
        this.count = count;
    }
    public String getPrice() {
        return price;
    }
    public void setPrice(String price) {
        this.price = price;
    }
}

次にフォーム用のJavaBeanとして、ApplicationFormを用意します。

public class ApplicationForm {

    private String name;
    private String contents;
    private List<ProductBean> products;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getContents() {
        return contents;
    }
    public void setContents(String contents) {
        this.contents = contents;
    }
    public List<ProductBean> getProducts() {
        return products;
    }
    public void setProducts(List<ProductBean> products) {
        this.products = products;
    }

・・・

また、ApplicationFormにはvue.jsへ値を受け渡すために、以下のような、自身をJSONに変換するメソッドを実装します。

   @JsonIgnore
    public String getJson() throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(this);
    }

そして、以下のようにThymeleaf側のvue.jsスクリプトの初期値として、フォームを渡すようにします。 ここで、JavaScript内でThymeleafの変数にアクセスするには、 Tutorial: Using Thymeleaf (ja) にあるように、scriptタグにth:inline="javascript"を付けて行うようです。

<script th:inline="javascript">
var vm =  new Vue({
    el: '#application',
    data: /*[(${applicationForm.json})]*/message: 'Hello Vue!',

・・・

また、リアクティブに計算する処理も実装します。

   computed: {
        subtotal: function() {
            return function(product) {
                if(product.count==null||product.price==null){
                    return "---";
                }
                return product.count * product.price;
            }
        },
        total: function() {
            var total = 0;
            for(const p of this.products){
                if(p.count==null||p.price==null){
                    continue;
                }
                total = total + p.count * p.price;
            }
            if(total==0){
                return "---";
            }
            return total;
        }    
    },

実際は他にもvue.jsで動的に行いたい処理を実装しています。

最後にThymeleafでフォームを実装します。 vueオブジェクトをフォームにアタッチするために、id="application"を付加しています。

<form th:action="@{/application/confirm}" th:object="${applicationForm}" method="post" id="application">
  <dl class="formlist">
    <dt>今回の実施名をご記入ください<span class="required">※必須</span></dt>
    <dd><input class="form-control" type="text" v-model="name" name="name" required /></dd>
    <dt>内容をご記入ください<span class="required">※必須</span></dt>
    <dd><textarea class="form-control apply" v-model="contents" name="contents"></textarea></dd>

    <dt>商品の内訳をご記入ください。</dt>
    <dd>
      <table class="product-set">
        <tr>
          <th></th>
          <th>商品名</th>
          <th style="width:120px;">商品単価</th>
          <th></th>
          <th style="width:90px;">梱包数</th>
          <th>商品小計</th>
        </tr>
        <tr v-for="(product, index) in products">
          <td>{{ index+1 }}</td>
          <td><input type="text" class="form-control" v-model="product.name" v-bind:name="'products['+index+'].name'" /></td>
          <td><input type="number" class="form-control" v-model="product.price" v-bind:name="'products['+index+'].price'" /></td>
          <td class="center">×</td>
          <td><input type="number" class="form-control" v-model="product.count" v-bind:name="'products['+index+'].count'" /></td>
          <td class="subtotal">¥ {{ subtotal(product) }}</td>
        </tr>
        <tr class="total-row">
          <td class="center" colspan="5">合計</td>
          <td class="total">¥ {{ total }}</td>
        </tr>    
      </table>             
    </dd>
  </dl>
  <div class="block block-center">
    <input class="btn btn-primary btn-lg" type="submit" value="申込内容を確認する" />
  </div>
</form>

上記のように、inputのname属性にJavaBeanのプロパティ名を指定することで、POSTした際にSpringBootへ値を渡すことができます。 また、JavaBeanのプロパティがリスト型の場合は、v-bind:name="'products['+index+'].name'" のようにすることで、一覧として値を渡すことができます。

このやり方をするとDOMの扱いを意識しないで良いので、動的なフォームを作るのがとても楽になりました。 ただ複雑になってくると、Thymeleafのth:とvue.jsのv-bindが入り混じって、どっちの何だか混乱してきます。

まあ、古くはJSP上でのJavaスクリプトレット+JavaScriptのころから、入り混じって混乱しがちでしたが…。

以上です。