Spring + Vue.jsでページングする
Spring Dataを使うと、Pagingをいい感じにやってくれる。こいつとVue.jsを使って、いい感じにページングをしてみる。
API
SpringのRestController
とPageable
が全てよしなに取り計らってくれる。
@RequestMapping("/api") @RestController public class APIController { @Autowired private ItemRepository repo; @GetMapping("/items") public Page<Item> allItems(Pageable pageable) { return repo.findAll(pageable); } }
これで、以下のようにページングしやすい形でResponseを返してくれる。
{ content: [ ... (省略) ...], pageable: { sort: { sorted: false, unsorted: true }, offset: 200, pageSize: 20, pageNumber: 10, unpaged: false, paged: true }, totalPages: 21, totalElements: 410, last: false, size: 20, numberOfElements: 20, first: false, sort: { sorted: false, unsorted: true }, number: 10 }
英語だけど以下の記事がわかりやすい。HATEOAS 対応しようとしたけど、面倒くさそうなので後回し。
画面
上記のAPIで返ってきた結果を表示する。Vue.js始めたてなので、もっとスマートなやり方はあるかも。
まずはページングするためのComponent。単純に全ページ分を表示する。20ページを超えたあたりからちょっと見づらくなるかも。
個人的にポイントだと思ったのは以下。
RestController
が返すJSONのうち、content
以外をとりあえずpager
というpropsにして渡す。その後の制御は大体これで事足りる。- JSONに含まれるページ番号は0始まりのインデクスだが、
v-for="page in page.totalPages"
でループするpage
は1始まり。 - 各ページがアクティブ(=現在ページ)かどうかを判定するために、インラインで
{'active': page - 1 === pager.number}
としている。 - ページ遷移をするときは、propsで渡された
pager
自身を更新する必要があるので、親Componentとやり取りする必要がある。調べた限りだと以下の方法がありそう。- 親Componentをpropsで子Componentにわたす
$emit
でイベントを発行し、そいつを親Componentで捕まえる (ここの解説)
Vue.component('pager-block', { props: ['pager'], data: {}, computed: {}, methods: { changepage: function(page) { this.$emit('changepage', page); } }, template: ` <nav aria-label="Page navigation example"> <ul class="pagination"> <li class="page-item" :class="{disabled: pager.first}"> <a class="page-link" href="#">Previous</a> </li> <template v-for="page in pager.totalPages"> <li class='page-item' :class="{'active': page - 1 === pager.number}"> <a class="page-link" href="#" :class="page" @click="changepage(page - 1)">{{page}}</a> </li> </template> <li class="page-item" :class="{disabled: pager.last}"> <a class="page-link" href="#">Next</a> </li> </ul> </nav> `, });
親Componentはこんな感じ。
methods
でページ読み込みの処理を作っておいて、mounted
から呼び出すGET
リクエストのパラメーターはURLSearchParams
を使う。普通に文字列として追加してもよい。- 上の方ので発行したイベントを、
@changepage
でキャッチする。 - propsで渡す
pager
にJSONの中身をぶち込むときに、普通にインデックスを使うとVue.jsが認識できない。this.$set
を使う必要がある (ここ参照)
<pager-block :pager="pager" @changepage="changepage"></pager-block>
var app = new Vue({ el: '#app', data: function() { return { itemList: [], pager: {}, }; }, methods: { loadPage: function(page) { var urlParams = new URLSearchParams(); urlParams.set("page", page); fetch("/api/items?" + urlParams.toString()) .then(response => {return response.json();}) .then(data => { console.log(data); for (var k in data) { if (k === 'content') { continue; } this.$set(this.pager, k, data[k]); } for (var i = 0; i < data.content.length; i++) { this.itemList.push(data.content[i]); } }); }, changepage: function(page) { this.itemList.splice(0); this.loadPage(page); } }, mounted: function() { this.loadPage(0); }, });