Vue.jsとvue-router.jsでpagerを作る

SPAを作るような目的ではなく、ページの一部としてvueを用いたページャーがあったら便利そうだなと思ったのと、nodeと一緒にvue.js使う例はあるのですが、サーバーを使わないでhtmlファイルのみですぐ確認できるものを作ってみたかったこともありまして作ってみました。

試行錯誤しながら作りましたので、スマートなコードではないかもしれませんが置いておきます。何かのヒントになれば幸いです。

## 概要

QiitaのAPIを使用して記事のリストをaxiosで取得して表示してそれにページング機能を追加したものです。

CodePen上とQiita上ではブラウザバックの動作は問題ありませんが、更新時のページ保持が動作しないようです。

<p data-height="750" data-theme-id="0" data-slug-hash="qoGQRm" data-default-tab="html,result" data-user="horikeso" data-embed-version="2" data-pen-title="VueRouterPagenation" class="codepen">See the Pen <a href="https://codepen.io/horikeso/pen/qoGQRm/">VueRouterPagenation</a> by horikeso (<a href="https://codepen.io/horikeso">@horikeso</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 確認時の注意

[API v2](https://qiita.com/api/v2/docs)

認証している状態ではユーザごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。

pageの初期値は1、pageの最大値は100に設定されています。また、per_pageの初期値は20、per_pageの最大値は100に設定されています。

ということなのでいじっていると途中で取得できなくなるかもしれません。

page=101から
```{"message":"Bad request","type":"bad_request"}```

per_page=101から
```{"message":"Rate limit exceeded","type":"rate_limit_exceeded"}```

### すぐ確認できなくなるので個人のアクセストークンで認証しています。

read_qiita - Qiitaからアクセストークンに紐付いたユーザに関連したデータを読み出す

[JSON Schema](https://qiita.com/api/v2/schema)

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>VueRouterPagenation</title>
<script src="https://unpkg.com/vue@2.5.16/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-router@2.7.0/dist/vue-router.js"></script>
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<nav>
<ul>
  <router-link v-if="basePageId > 1" tag="li" :to="{ path: 'page', query: { action: 'pre' + basePageId }}"><a>&#60;</a></router-link>
  <router-link v-if="basePageId <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: (basePageId).toString() }}"><a v-if="currentPage === basePageId"><span style="color:green;">{{ basePageId }}</span></a><a v-else>{{ basePageId }}</a></router-link>
  <router-link v-if="basePageId + 1 <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: (basePageId + 1).toString() }}"><a v-if="currentPage === basePageId + 1"><span style="color:green;">{{ basePageId + 1 }}</span></a><a v-else>{{ basePageId + 1 }}</a></router-link>
  <router-link v-if="basePageId + 2 <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: (basePageId + 2).toString() }}"><a v-if="currentPage === basePageId + 2"><span style="color:green;">{{ basePageId + 2 }}</span></a><a v-else>{{ basePageId + 2 }}</a></router-link>
  <router-link v-if="basePageId + 3 <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: (basePageId + 3).toString() }}"><a v-if="currentPage === basePageId + 3"><span style="color:green;">{{ basePageId + 3 }}</span></a><a v-else>{{ basePageId + 3 }}</a></router-link>
  <router-link v-if="basePageId + 4 <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: (basePageId + 4).toString() }}"><a v-if="currentPage === basePageId + 4"><span style="color:green;">{{ basePageId + 4 }}</span></a><a v-else>{{ basePageId + 4 }}</a></router-link>
  <router-link v-if="basePageId + 5 <= parseInt(articleCount / pageUnit, 10)" tag="li" :to="{ path: 'page', query: { action: 'next' + basePageId }}"><a>&#62;</a></router-link>
</ul>
</nav>
<router-view></router-view>
</div>
</body>
</html>
```

```js
// Components
const Page = { template: '\
    <div v-if="$root.linkList.length">\
        <div v-html="link" v-for="link in $root.linkList">{{ link }}</div>\
    </div>\
    <div v-else-if="$root.errorFlag">リストの取得に失敗しました</div>\
    <div v-else>Loading...</div>'
};

// Router
const router = new VueRouter({
    routes: [
        { path: '/page', component: Page}
    ]
});

document.addEventListener('DOMContentLoaded', function() {
    const app = new Vue({
        el: '#app',
        data: {
            basePageId: 1,
            pagerUnit: 5,
            pageUnit: 25,
            articleCount: null,
            currentPage: 1,
            errorFlag: false,
            pattern: new RegExp("^[0-9]*$"),
            linkList: []
        },
        created () {
            this.articleCount = this.pageUnit * 100;// 100ページまでしか取得できないので
            this.getPage();
        },
        watch: {
            // ルート変更時
            '$route': 'getPage'
        },
        methods: {
            getPage() {

                this.errorFlag = false;// 初期化
                this.linkList = [];// 初期化
                if (typeof this.$route.query.action === "undefined") {
                    location.href = "qoGQRm#/page?action=" + this.basePageId;
                    this.actionCount++;
                    return;
                }

                const currentPageId = this.basePageId;

                if (this.$route.query.action.indexOf("pre") !== -1) {
                    this.$route.query.action = this.$route.query.action.replace(/pre/g, "");
                    if (this.pattern.test(this.$route.query.action) === true && parseInt(this.$route.query.action, 10) > 0) {
                        this.$route.query.action = (parseInt(this.$route.query.action, 10) - this.pagerUnit).toString();
                        this.basePageId = Math.ceil((parseInt(this.$route.query.action, 10) - this.pagerUnit) / this.pagerUnit);
                    } else {
                        this.basePageId = 1;
                        this.$route.query.action = "1";
                    }
                }

                if (this.$route.query.action.indexOf("next") !== -1) {
                    this.$route.query.action = this.$route.query.action.replace(/next/g, "");
                    if (this.pattern.test(this.$route.query.action) === true && parseInt(this.$route.query.action, 10) > 0) {
                        this.$route.query.action = (parseInt(this.$route.query.action, 10) + this.pagerUnit).toString();
                        this.basePageId = Math.ceil((parseInt(this.$route.query.action, 10) + this.pagerUnit) / this.pagerUnit);
                    } else {
                        this.basePageId = 1;
                        this.$route.query.action = "1";
                    }
                }

                if (this.pattern.test(this.$route.query.action) !== true) {
                    this.errorFlag = true;
                    return;
                }

                // 現在位置判定用保持
                this.currentPage = parseInt(this.$route.query.action, 10);

                // ブラウザリロード時に位置を保持する対策
                this.basePageId = parseInt((parseInt(this.$route.query.action, 10) - 1) / this.pagerUnit, 10) * this.pagerUnit + 1;

                axios.get("https://qiita.com/api/v2/items?page=" + this.currentPage + "&per_page=" + this.pageUnit, { 'headers': { 'Authorization': 'Bearer 0db1d5ad3a19263f1e199e78b1d429b173acd2a8' }})
                    .then(result => {
                        const objectList = result.data;
                        let linkList = [];
                        objectList.forEach(function (object) {
                            linkList.push('<a href="' + object.url + '" target="_blank" rel="noopener">' + object.title + '</a>');
                        });
                        this.linkList = linkList;
                    })
                    .catch(error => {
                        this.errorFlag = true;
                    });
            }
        },
        router
    });
});
```

```css
#app ul {
    list-style-type: none;
    display: flex;
} 
#app li + li {
  margin-left: 10px;
}
```

# 参考
https://jp.vuejs.org/v2/guide/index.html
https://router.vuejs.org/ja/essentials/getting-started.html