Vueで外部などからデータを取得する場合、axiosを用いることが多いですが、このaxiosのデータはプロキシ制御がかかってなかなか自在にデータを操作できないようで、海外でも多くのエンジニアが悪戦苦闘しているようです。
この記事はaxiosで取得したデータを同一コンポーネント内に展開させる方法、そしてprovideとinjectを使ってaxiosで取得したデータを展開させる方法、更にバックエンド(Laravel)に対するCRUD制御の方法を紹介していきます。
今から実践しようとしていることは、よくあるapi/teamsパスからDBデータの取得とCRUD操作です。使用しているデータベースはsqlite3、またLaravel10のコントローラにつなげています。以下の記事を参考にしています。
参考先の記事ではoptions APIで記述していますが、本記事はscript setupで記述しています。
使用環境
使用環境は以下の通りです
- OS: AlmaLINUX9.1
- PHP: ver8.2
- フロントエンド: Vue3.3(Vite)
- バックエンド: Laravel10
- DB: sqlite3
※ Docker、TypeScript不使用
※ところで、昨今ではフロントエンドにVue、バックエンドにLaravelという組み合わせが人気ですが、それには根拠があります。
- Vue フォーム制御を円滑化する目的で開発されたAngularJSを改良したライブラリなので、フォーム制御が簡単。処理が高速。ただし大規模化、データ保存には向かない。
- Laravel DBとの親和性がありデータ保存にも向いている。ルーティングもわかりやすく、機能拡張もしやすい。ただし処理が遅く、フォーム制御も分かりづらい。
なので、お互いの欠点を見事に補完してくれます(驚くぐらい超高速です)。そして、それを媒介してくれる便利なライブラリがaxiosです。
SQLiteで管理しているDBテーブルについて
sqliteで管理しているDBテーブルはteamsというもので、NFL、NBA、MLB、NHL、MLS5団体のスポーツチーム(154件)が格納されています。また主キーをコードで制御
しているので、マイグレートの際にinsertメソッドを使用しています(後の処理でこの部分の把握が重要になってきます)。
カラム名 | 説明 | 参照値 |
---|---|---|
id | カテゴリコード+連番2桁 | 101、330など(1:NFL、2:NBA…) |
kind | 競技団体名 | NFL、MLBなど |
name | チーム名 | デンバー・ナゲッツなど |
content | 概要 | チーム紹介 |
area | カンファレンス | ナショナルリーグ東地区など |
place | 所在地 | ロサンゼルスなど |
home | ホームスタジアム | マディソン・スクエア・ガーデンなど |
同一コンポーネントでテンプレート上に展開させる
同一コンポーネントのテンプレート上に展開させるだけならrefで問題ありません。
axiosのデータ取得
axiosからのデータはgetメソッドで簡単に取得できます。以下のようにrefメソッドに代入します。ただし、この方法だとproxy規制に引っかかるのでもうひと工夫必要です。
cosnt data = ref()
await axios.get('/api/teams').then((res) =>{ data.value = res.data } )
ライフサイクルフックで非同期処理する
そこでライフサイクルフック(今回はonMounted)を用いて非同期処理を行います。データ取得用のメソッドに対してasyncとawaitを活用することで、同一コンポーネント内に展開することができます。
<script setup>
import axios from 'axios';
import {ref,onMounted} from 'vue';
const teams = ref();
const getTeams = ()=>{
axios.get('/api/teams').then((res) =>{
teams.value = res.data;
})
}
//DOM読み込み後に展開する
onMounted(async()=>{
await getTeams();
})
</script>
これで同一コンポーネント内のテンプレートに展開することができます。
<template>
<ul v-for="y in teams">
<li>{{y.name}}</li>
</ul>
</template>
<script setup>
import axios from 'axios';
import {ref,onMounted,provide} from 'vue';
const teams = ref();
const getTeams = async ()=>{
await axios.get('/api/teams').then((res) =>{
teams.value = res.data
})
}
onMounted(async()=>{
await getTeams();
})
</script>
axiosの値を参照したい場合
axiosの値を参照したい場合は以下のように、メソッドに対して戻り値を返せるように記述します。ここから処理を行って、refやreactiveを用いてテンプレート上に任意の値を返すこともできます。
※この展開した値は後述するprovideに代入してもinjectできません。
<script setup>
import {useRouter} from 'vue-router';
import axios from 'axios';
import {ref,onMounted,provide} from 'vue';
const router = useRouter();
const getTeams = async ()=>{
return await axios.get('/api/teams').then((res) =>{
return res.data;
})
}
onMounted(async()=>{
const data = await getTeams();
console.log(data) //値が展開されている
})
</script>
2:provideとinjectを用いてSPA内でデータをやり取りする
ここからが本題です。SPAを用いた兄弟コンポーネントにデータを受け渡すにはprovideとinjectを用いると非常に便利ですが、provideには大きな制約があります。
必ず、setup関数直下で代入すること
リアクティブな値を代入すること
したがって、ライフサイクルフック内でprovideを使用しても値が転送されませんので、ライフサイクルフック内で取得した値をsetup関数内に再展開する必要があります。そこでライフサイクルフック内で一旦、reactive制御した変数stateに一旦代入しておきます。また当然ですが、provideとinjectでデータのやり取りできるのは同一のルーティング内だけです。
※refではなくてreactiveを使用して受け渡しします。理由は後述。
<script setup>
import {reactive,onMounted,provide} from 'vue';
const state = reactive({teams:[]}); //teamsプロパティを準備しておく
onMounted(async()=>{
await getTeams();
})
</script>
あとはprovideでstateを用意しておきます。ここで注意しなければいけないのはリアクティブな変数を代入する必要があるため、展開された値であるstate.teamsを送出しても受取先で展開できません。また、空のreactiveを作成してObject.assignを使って代入する方法もありますが、その方法を採用した場合は展開した値の同期が取れなくなります(参照させるだけなら大丈夫ですが)。
※指摘があったのでgetTeamsのasyncとawaitは外しています。
受け渡し元(兄コンポーネント)
<script setup>
import {useRouter} from 'vue-router';
import axios from 'axios';
import {reactive,onMounted,provide} from 'vue';
const state = reactive({teams:''});
const router = useRouter();
const getTeams = ()=>{
axios.get('/api/teams').then((res) =>{
state.teams = res.data; //リアクティブな変数stateに代入
})
}
onMounted(async()=>{
await getTeams();
})
provide('teams',state); //provideはsetup直下で使用すること。
</script>
受取用のinjectはシンプルに処理できます。子コンポーネントのインポートを忘れないようにしましょう(ループが実施されない上に、子コンポーネントを呼び出していないというエラーも出ません)。
受取先(弟コンポーネント)
<template>
<div class="container">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th scope="col">#</th>
<th scope="col">名称</th>
<th scope="col">概要</th>
<th scope="col">場所</th>
<th scope="col">修正</th>
<th scope="col">削除</th>
</tr>
</thead>
<tbody>
<TeamItem
v-for="(team,idx) in data.teams"
:key="idx"
:team="team"
/>
</tbody>
</table>
</div>
</template>
<script setup>
import { inject } from 'vue';
import TeamItem from './TeamItemComponent.vue'; //子コンポーネントのインポートを忘れない
const data = inject('teams');
</script>
CRUD操作する
このprovideとinjectを使って、データのCRUDを行います。繰り返しますがCRUDを実行できるのは変数をリアクティブにした場合のみです(Object.assignだと同期できない)。
ちなみに、各フォームの値は説明上、冗長になってしまうので省略していますが、v-modelディレクティブに紐づいたフォームの値を回収することになります。
また、フロントエンド内でのオブジェクトのやりとりだけでなく、バックエンドによるDBテーブル操作も絡んでくるため、登録、修正、削除の操作において、それぞれコマンドの差違が発生します(データ再読込の方法が異なるため)。
例:登録のnameプロパティの場合
<div class="form-group row border-bottom">
<label for="title" class="col-sm-3 col-form-label">名称</label>
<input type="text" class="col-sm-9 form-control-plaintext" id="title" v-model="team.name">
</div>
axiosのおさらい
その前にaxiosのおさらいをしておきます。axiosとはhttps上のリクエストをjson形式で取得する仕組みであり、DBテーブルの登録のため、フロントエンド(Vue)で操作したオブジェクトをjson化し、そのデータを送信したり、受信したりします。
用途 | 説明 |
---|---|
取得 | getを用いる。getはリクエスト上のデータを受け取る(クエリパラメータを送るgetとは性格が異なるものと認識した方がよい)。 |
登録 | postを用いてパラメータをリクエストに送る。postはフォームタグが持っているので、それを借用する。postはリクエストを再受信するため、更新後のデータが呼び出される。 |
修正 | putを用いる。putはgetと同様リクエスト上のパラメータを受け取るものだが、jsonデータをプレースホルダにして送ることもできる。putはフォームタグが持っていないので、クリックイベントで命令を賄う。また、対象idはパスパラメータに埋め込む。 |
削除 | deleteを用いる。deleteもputと同様、フォームタグが持っていないのでクリックイベントで命令を賄う。また、deleteはデータをリクエストに返さない。 |
それから、いずれの操作にしても、命令実行後に再ロードによるデータ再取得の手順が必要になります。これは、DBテーブルの操作をしているのはあくまでLaravelというバックエンドに過ぎないからです(JSフレームワーク単体のSPAでaxios操作している場合との最大の違い)。
つまり、データ送信は
Vueでデータ操作 → axiosによってコマンドとjsonデータをリクエスト上に送る → 送られたリクエストをLaravel上のルーティングで命令を振り分け → Laravel上のコントローラでDBテーブルを操作し、各処理を行う
データ受信は
リクエストの再ロード → axiosによってgetコマンドをリクエスト上に送る → 送られたgetリクエストをLaravel上のルーティングで命令を振り分け → Laravel上のコントローラでDBを操作し、クエリをjsonデータ化する → getコマンドのレスポンスをオブジェクトに代入する → フロントエンド上にデータを受け流す
という流れになります。
データを登録する
新規に登録する場合はaxios.postを用います。また、v-modelに紐づける変数ですがあらかじめreactiveで設定しておきましょう(TypeScriptだと事前に型を代入するだけで定義できますが、今回は使用していないので)。フォーム制御はv:onディレクティブでメソッドを呼び出しています。
登録の場合は、フォームタグが持っているpostを使用するのでsubmitを発動させます。submitを発動させることによって、バックエンド側のPHPは再ロードされるのでデータが刷新されるという動きとなります。
一方、submitを実行するとrouter.pushが無効となるようです。代替手段として、ルートコンポーネントにrouter.go(-1)としておけばページ遷移できます。route.go(履歴)は履歴を操作するので、登録前のページ、つまりは一覧ページに遷移します。
<script setup>
onMounted(async()=>{
await getTeams();
router.push("/teams"); //登録後ルートコンポーネントに遷移するので、そこから一覧へ遷移
})
</script>
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-6">
<form>
<!-- 中略 -->
<button class="btn btn-primary" @click="onSubmit">登録</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import axios from 'axios';
import {reactive} from 'vue';
import {useRouter} from 'vue-router';
const router = useRouter();
//v-modelに紐づけるためのフォームの初期値
const team = reactive({
title: '',
kind: '',
name: '',
/*省略*/
})
const onSubmit =async (e)=>{
await axios.post('/api/teams',team).then((res)=>{
if(res){
router.go(-1); //登録前の画面に戻る。router.push("/")はうまく動かない
}else{
console.log("ERR");
}
})
}
</script>
データを修正する
データを修正する場合はaxios.putを用います。修正はそこまで難解でなく、ルーティングもrouter.pushが普通に使えます。
修正の場合はput(フォームタグにはない独自の制御コマンド)なので、submitでなくボタンのクリックイベントだけです(このputもルーティングに制御しておきます)。一方、クリックイベントのときはpost時と異なりrouter.pushメソッドが普通に機能するので、システムも再読込してくれます。また、putはgetの働きも持つので、リクエストも返してくれます。
※修正の場合のみフロントエンド上でオブジェクトの同期が必要になります(そうしないとputが差分を認識しない)。バックエンド操作する変数をリアクティブにしているのはこの修正操作のためです。
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-6">
<form>
<!-- 中略 -->
<button type="button" class="btn btn-primary" @click="onSubmit">Submit</button>
</form>
</div>
</div>
</div>
<router-link to="/teams">
<button class="btn btn-success">戻る</button>
</router-link>
</template>
<script setup>
import axios from 'axios';
import {inject} from 'vue';
import {useRoute,useRouter} from 'vue-router';
const route = useRoute();
const router = useRouter();
const id = Number(route.params.id); //パラメータ上のプロパティを取得(ここではidと設定している)
const data = inject('teams'); //データの取得
const team = data.teams.find((item)=> item.id === id); //該当するデータを展開
const onSubmit = ()=>{
axios.put(`/api/teams/${id}`,team).then((res)=>{
if(res){
router.push("/teams")
}else{
console.log("ERR")
}
})
}
</script>
データを削除する
これが一番厄介で、deleteはリクエストでデータを返さないため、削除後、情報を新たにaxiosから削除済データを、手動で再取得する必要があります(バックエンドでDBテーブルを削除してもフロントエンドが変更を検知しない)。したがって、データ取得用のgetTeamsメソッドごとprovideに代入し、削除イベント後に読み込みさせます。
※削除ボタン自体は子コンポーネントに位置するので、defineEmits
コンパイラマクロでコマンドごと親コンポーネントに持ってきています。
ルートコンポーネント
<template>
<HeaderComponent />
<router-view />
</template>
<script setup>
import HeaderComponent from './HeaderComponent.vue'
import axios from 'axios';
import {reactive,onMounted,provide} from 'vue';
const state = reactive({teams:''})
const getTeams = async ()=>{
return await axios.get('/api/teams').then((res) =>{
state.teams = res.data;
})
}
onMounted(async()=>{
await getTeams();
})
provide('teams',state); //データの分配
provide('gteams',getTeams); //データ取得イベントの分配(削除時に必要)
</script>
親コンポーネント(Todoリスト)
<template>
<div class="container">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th scope="col">#</th>
<th scope="col">名称</th>
<th scope="col">概要</th>
<th scope="col">場所</th>
<th scope="col">修正</th>
<th scope="col">削除</th>
</tr>
</thead>
<tbody>
<TeamItem
v-for="(team,idx) in data.teams"
:key="idx"
:team="team"
@del="delTeam"
/>
</tbody>
</table>
</div>
</template>
<script setup>
import {inject } from 'vue';
import {useRouter} from 'vue-router';
import TeamItem from './TeamItemComponent.vue';
const router = useRouter();
const data = inject('teams');
const getTeams = inject('gteams');
//子コンポーネントから受け取った削除イベント
const delTeam = (sel_id)=>{
axios.delete(`/api/teams/${sel_id}`).then((res)=>{
if(res){
getTeams(); //データの再読み込み(injectで呼び出したルートコンポーネント上の関数)
alert("削除しました");
}
})
}
</script>
子コンポーネント(各種ToDo)
<template>
<tr>
<th scope="row">{{ props.team.id }}</th>
<td>{{ props.team.title }}</td>
<td>{{ props.team.content }}</td>
<td>{{ props.team.person }}</td>
<td>
<router-link :to="`edit/${props.team.id}`">
<button class="btn btn-success">修正</button>
</router-link>
</td>
<td>
<button class="btn btn-danger" @click="commandDel(props.team.id)">削除</button>
</td>
</tr>
</template>
<script setup>
const props = defineProps({team: String});
//削除イベントを親コンポーネントに渡す
const emit = defineEmits(["del"]);
const commandDel = ()=>{
emit("del",props.team.id)
}
</script>
【応用】コンポーザブルで機能を集約する
CRUD機能があちこちに分散しているのは保守困難となってくるので、そんな場合はコンポーザブルを作るといいです。コンポーザブルはuseHogeで作るのがセオリーのようなので(実際のところ、名前はなんでもいいらしい)、useAxiosというコンポーザブルを作成して、CRUD機能をまとめてみました。
<script>
import axios from 'axios';
import {inject} from 'vue';
import {useRouter} from 'vue-router';
export default function useAxios(mode){
const router = useRouter();
const getTeams = inject('gteams');
const addTeams = (team)=>{
axios.post('/api/teams',team).then(async (res)=>{
if(res){
//alert("登録しました");
}
})
}
const uploadTeams = (id,team)=>{
axios.put(`/api/teams/${id}`,team).then((res)=>{
if(res){
//alert("修正しました");
}
})
}
const deleteTeams = (id)=>{
axios.delete(`/api/teams/${id}`).then((res)=>{
if(res){
//alert("削除しました");
}
})
}
const sendmode = (id,dif)=>{
switch(mode){
case "add": addTeams(dif);
break;
case "upd": uploadTeams(id,dif);
break;
case "del": deleteTeams(id);
break;
}
getTeams(); //データの再読込
router.push('/teams');
}
return{sendmode}; //コンポーネントで使用する関数を返す
}
</script>
コンポーザブルの設定は以下のようにしています(登録ページの場合)。
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-6">
<form>
<!-- 中略 -->
<button type="button" class="btn btn-primary" @click="cp.sendmode(null,team)">登録</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import axios from 'axios';
import {reactive,inject} from 'vue';
import useAxios from './useAxios.vue'; //呼び出したコンポーザブル
const team = reactive({
title: '',
content: '',
person: ''
})
const cp = useAxios('add'); //使用したい機能を設定
</script>
この方法を使えば、兄弟、親子どのコンポーネントからも自在に機能を呼び出すことができます(インスタンスcpがコンポーザブル用のコールバック関数となっているので、後はコンポーザブル上でreturnに記述されたsendmodeメソッドを好きなタイミングで呼び出せる)。
※データ取得部分もコンポーザブルに集約しようとしたのですが、それだとどうしてもデータの同期が取れなかったので、試行錯誤中です。
axios.response.errorが発される場合
laravel上でモデルを構築している場合、以下のように設定しておかないと同期データを送れずにaxiosを操作した時点でエラーとなります(削除はidを取得するだけなので可能)。
また、シーダーのマイグレートにおいてcreateメソッドで作成した場合主キーは不要(厳密に言えば、createは主キーをシーケンス型にした場合で用いる)ですが、insertで作成した場合は主キーも必ず記述しておいてください(今回はコード制御なのでidカラムも記述しています)。
class Team extends Model
{
protected $fillable = [
'id',
'kind',
'name',
/*省略*/
];
}
データの受け渡しになぜrefを使用しないのか?
ref推奨派ならばデータの受け渡しにrefでいいんじゃないか?と思うことでしょう。ですがSPAを用いた場合は、動作パフォーマンスが大きく異なってきます。reactiveはストレージ管理用に用意された処理用メソッドでありデータを一括管理してくれるので、injectで値を受け取ろうが、どのコンポーネントにおいても同じ動きをします。対するrefは各コンポーネントに対するプリミティブな変数処理用なので、injectで値を受け取った場合に逐一、valueへの代入処理を繰り返す違いがあります。
すなわち、詳細画面などから一覧画面へ遷移する際、reactiveを使用していると値の展開だけを実行するので処理が速いのに対し、refを使用していた場合は毎回valueを代入して再処理しようとする(今回は後述するwatchEffectを使用しているため、尚更遅くなります)ので、処理が遅くなるのです。
【付録1】ページングを行う
データが膨大になってくると全部表示するのは時間がかかるかも知れない上に見づらいので、ページング(ページャーボタンを作成)を作ってみます。といってもJSフレームワークのぺージャーは見かけだけのページング機能なので、
現在のページにしたがって、表示したいデータを切り取る
これだけで問題なく動きます。流れとしては
現在のページを取得する → ページャーを作る・データを切り取る
となります。ただし、Vueならではの挙動の癖があるので、少しマイナーなライフサイクルフックや監視プロパティを駆使する必要があります。
実装したプログラムは以下のようになっており、onBeforeMountとwatchEffectがポイントです。
<template>
<div class="container">
</div>
<div class="container">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th scope="col">#</th>
<th scope="col">名称</th>
<th scope="col">概要</th>
<th scope="col">場所</th>
<th scope="col">修正</th>
<th scope="col">削除</th>
</tr>
</thead>
<tbody>
<TeamItem
v-for="(team,idx) in divided"
:key="idx"
:team="team"
/>
</tbody>
</table>
<div class="d-grid gap-2 d-md-block">
<router-link :to="`${kind}?page=${p}`" v-for="(p,i) in pages" :key="i" >
<button class="btn btn-primary btn-light btn-outline-dark" :class="is_active(p,page)">{{ p }}</button>
</router-link>
</div>
</div>
</template>
<script setup>
import {inject,onBeforeMount,watchEffect,ref} from 'vue';
import {useRoute} from 'vue-router';
import TeamItem from './TeamItemComponent.vue';
const data = inject('teams');
const page = ref(null); //現在のページ
const pages = ref([]); //ページャーリスト
const divided = ref(null); //ページャーにしたがって抽出されたデータ
const kind = ref();
const route = useRoute();
//選択されたページをアクティブにする
const is_active = (page_num,c_page)=>{
let act_flg = false;
if(page_num === c_page){
act_flg = "active";
}else{
act_flg = null;
}
return act_flg;
}
//データの切り取り
const slice_data = (pg,sel_data)=>{
const start = (pg - 1)* 10; //データ取得の初期値インデックス
const end = pg * 10; //データ取得の末端値インデックス
return sel_data.slice(start,end);
}
//ページャー作成
const make_pager = (data)=>{
//ページ数計算
const per_row = 10; //表示する行数
let cnt_page = Math.ceil(data.length / per_row); //表示に必要な行数
//ページ作成
const ar_pages = [];
for(let i = 0 ;i < cnt_page ; i++ ){
ar_pages.push(i + 1);
}
return ar_pages;
}
//ロード時のページ設定
onBeforeMount(()=>{
const c_page = route.query.page;
if(c_page !== undefined){
page.value = c_page; //遷移先からのクエリ受取
}else{
page.value = 1; //ページの初期値
}
})
//取得ページ数にしたがってデータを切り取る
watchEffect(()=>{
//ページャー作成
pages.value = make_pager(sel_data);
//データの切り取り
divided.value = slice_data(page.value,sel_data);
})
</script>
ページ数を取得
まずは現在のページ数を取得する必要がある(初期値なら1、ページ遷移ボタンを押したときはその値、または遷移先から戻ったときはクエリから取得)ので、ライフサイクルフックの一つ、onBeforeMount(テンプレート呼出前に呼び出す)によって画面遷移直後にページ数を取得します。
※一般に知られるライフサイクルフックはonMountedですが、これはテンプレート呼出後に処理されるため、データ表示制御が間に合いません。
ページ数を監視する
ページ数を取得したら次はページャー作成とデータの切り取り作業に入りますが、ここで注意点があります。必ず、監視プロパティのwatchEffectでpage.valueの値の変化を監視しておきましょう。
※ちなみに監視プロパティだとwatchが知られていますが、それだと値の変更があった場合しか反応しません。ですが、watchEffectは代入というイベントが起きた時点で値の変更の有無にかかわらず監視対象となるので、同一ページの詳細ページ等から戻った場合もデータ切り取りイベントを実行してくれるのです。
ページャー作成
そのページ数にしたがってページャー(ページリンク)を作成します。表示に必要な行数はMath.ceil(データ数/行数) で求められるので、その値を上限にv-forディレクティブでループさせるためのページオブジェクトを作成し、変数pageに代入すれば簡単に作れます。
ページリンク用のパスの書き方
ページボタンはrouter-linkタグで作っておくといいです。そしてパスですがスラッシュを省略した場合、クエリの場合はカレントパスに追記してくれるので、以下のように記述しておくだけで問題ありません。つまり、ページ数が3ならば
teams?page=3
となります。
アクティブなページボタンをダイナミックに変化させる
デザインはbootstrapで制御しているので、クラス属性を足し引きするだけで大丈夫です。v-bind:classディレクティブを活用すれば簡単です。class="active"となれば、ボタンが選択された状態で表示されます。
※判定文も念の為双方Number型にしてしまえば確実です。
<button class="btn btn-primary btn-light btn-outline-dark" :class="is_active(p,page)">{{ p }}</button>
<script setup>
//ページリンクの番号と現在のページ番号が一致していれば、activeクラスを付与する(activeは、選択されたボタンを示すbootstrapの定義クラス)。
const is_active = (page_num,c_page)=>{
let act_flg = false;
if(Number(page_num) === Number(c_page)){
act_flg = "active";
}else{
act_flg = null;
}
return act_flg;
}
</script>
リンクボタンの記述は以下のようになっており、v-forでループしています。また:classで制御されたis_activeメソッドの結果をclass属性に反映していきます(現在ページと一致すればactiveとなる)。
<router-link :to="`${kind}?page=${p}`" v-for="(p,i) in pages" :key="i" >
<button class="btn btn-primary btn-light btn-outline-dark" :class="is_active(p,page)">{{ p }}</button>
</router-link>
データを切り取る
ページ数が定まれば、そのページ数にしたがってデータを切り取るだけなのでarray.sliceメソッドを使えば簡単です。ただし、テンプレートに展開する必要があるので、メソッドで取得した変数はrefを使用するのを忘れないようにしてください。また、同一ページの詳細ページに戻った場合、再度データを切り取る必要があるので前述したようにWatchEffectで監視しておかないと、データ表示できなくなる(watchだと詳細ページなどから戻った場合に、記憶しているページ数が同じために変更を感知しない
)ことがあります。
const per = 10; 表示させたい行数
const divided = ref(null); //ページャーにしたがって抽出されたデータ
const slice_data = (pg,sel_data)=>{
const start = (pg - 1)* per; //データ取得の初期値インデックス
const end = pg * per; //データ取得の末端値インデックス
return sel_data.slice(start,end); //表示したい分だけ切り取る
}
【付録2】サブディレクトリで表示させる
このシステムにカテゴリを元にしたサブディレクトリ(NFL、NBA、MLB、NHL、MLS)を設けてみます。サブディレクトリを設けた場合、色々と注意点が増えます。
ルーティング情報の変更
ひとまず、サブディレクトリがブラウザで表示できるようにVueのルーティング情報を書き換えておきましょう。
:kindにはカテゴリの5つ値のいずれかが入ります。ですが、それらは5種類あるので、それらが全部表示できるように正規表現で対応しておきましょう。パラメータをダイナミックに制御したい場合、直後に丸括弧で括れば、その中身がRegex(正規表現)となります。
path: ':任意のパラメータ(正規表現)'
//ルーティング設定
const routes = [
{
path: '/teams/:kind(\\S+)?',
name: 'TeamList',
component: TeamListComponent,
},
];
これで /teamsとなった場合でも、サブディレクトリがついて/teams/NFLとなった場合でも表示可能となります。厳密に5つ以外を通さない記述もできますが、敢えて実施していません。
正規表現について
\Sは空白以外の一文字、+は1文字以上、()は任意のグループ、そして?は0回か1回の繰り返しとなります。つまり、サブディレクトリが存在しない場合でも対応しておくことが大事です。
種別を絞り込む
サブディレクトリでも表示できるように制御したら、今度は呼び出したデータに対し、種別を絞り込めるようにします。
//データの絞り込み
<script setup>
//中略
const select_data = (t_data)=>{
kind.value = route.params.kind;
if(kind.value !== '' && kind.value !== undefined ){
return t_data.filter((item,idx)=>{
return item.kind === kind.value;
})
}else{
return t_data;
}
}
</script>
上記の種別絞り込みをページ数取得後に実行します。順番が逆転するとページリンクがおかしなことになります。
<script setup>
//取得ページ数にしたがってデータを切り取る
watchEffect(()=>{
page.value = route.query.page; //現在のページ
//データの絞り込み
const sel_data = select_data(data.teams);
//ページャー作成
let cnt_data = 0
if(Array.isArray(sel_data)){
cnt_data = sel_data.length
}
pages.value = make_pager(cnt_data);
//データの切り取り
divided.value = slice_data(page.value,sel_data);
})
</script>
あとはジャンル絞り込み用のボタンを作っておくといいでしょう。パスからは種別を、クエリからはページ数を取得していくようにすれば、親カテゴリからでも、パスありのサブディレクトリからでもページ数取得のクエリ操作が可能です。
<template>
<div class="d-md-block d-grid gap-2">
<router-link to="/teams/NFL?page=1">
<button class="btn btn-dark">NFL</button>
</router-link>
</div>
</template>
以下、作成中