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のころから、入り混じって混乱しがちでしたが…。
以上です。