やること
- 全レコード表示用APIを作成する
- 検索用APIを作成する
- 検索条件に基づきurlを変化させる
- ページ送りをクリックしても検索条件を維持する
- リロード時にも検索条件を維持(urlのクエリから検索結果を表示)
- 検索時に1ページ目に戻る
- 検索時に一番上までスクロールする
- ページ送り時に一番上までスクロールする
- ブラウザバックを検出した時にも表示内容をちゃんと連動させる
- 「○○」の検索結果 と表示する
- 「〇件ヒット」と表示する
- エンターキーを押したときにも検索が走るようにする
- ページネーションの表示ルール
…
や>
や<
>>
<<
などの表示条件を決める←CSSフレームワークに頼ったので、今回は深く触れていない
だるすぎワロタwwww
フレームワーク側でええ感じにやってくれよwww
全部書くわ
ジャンプできる目次
完成イメージ
思ってたのと違ったら帰れ!
これを最初に書いてくれないから、作ってみたら思ってたのと全然ちゃうやんけ!みたいな記事多すぎな。
前提条件
- Vue2とLaravelはインストール済み(今回はLaravel8でやってみた)
- SPAは構築済みとする
- 検索ボックスに何も入れなければ全レコードを表示
- 検索ボックスに何か入力されていればそれを使って該当レコードだけを表示
- CSSフレームワークはvuetify.jsを利用(それ以外のCSSフレームワークの場合でも参考にはなるかも)
バックエンド(LaravelのAPI)
//api.php
<?php
use App\Models\Inquiry;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\InquiryController;
Route::get('/inquiries', [InquiryController::class, 'index']);//レコードを取得
Route::get('/inquiries/search', [InquiryController::class, 'search']);//検索条件に当てはまるレコードを取得
この記事では、InquiryとかInquiriesって言葉がやたら出てくるけど、「問い合わせ」って意味。
好きな言葉に変えてくれていい。
//InquiryController.php
<?php
namespace App\Http\Controllers;
use App\Models\Inquiry;
use Illuminate\Http\Request;
class InquiryController extends Controller
{
public function index(Inquiry,$inquiry){
//Inquiriesテーブルのレコードの内容を新しい順に全件取得(20件ずつのページネーション)
$inquiry = $inquiry::orderBy('created_at', 'desc')->paginate(20);
return $inquiry;
}
public function search(Request $request){
//検索条件に合致するInquiriesテーブルのレコードの内容を新しい順に取得(20件ずつのページネーション)
$inquiry = Inquiry::query();
//部分一致検索とする
$pat = '%' . addcslashes($request->key, '%_\\') . '%';
if($request->key){//検索キーワードがあるなら
$inquiry->where(function ($query) use ($pat) {
$query->where('answer','LIKE',$pat);//answerカラムが検索キーワードと部分一致するレコードを取得
});
}
return $inquiry->orderBy('created_at', 'desc') ->paginate(20); //新しい順に取得(20件ずつのページネーション)
}
}
あれ?これ全件表示するindexメソッドと、検索のsearchメソッド分ける意味あるか???
まあ名前が分かりにくくなるのでこのまま行こう。
フロント(Vue.js)
今回はArhive.vueってファイルを使うことにする。
//Archive.vueのdata部分
data() {
inquiries: [],//レコード軍がこの後格納される配列
total:null,//ヒットしたレコードの数
current_page: 1,//現在のページ番号(初期値は1)
key: null,//検索キーワード
searchKeyword: null, //「○○」の検索結果に使う変数
}
//Archive.vueのhtml部分
<template>
<div>
<p v-if="searchKeyword">「{{searchKeyword}}」の検索結果 {{total}}件ヒット</p>
<p v-else>全件表示</p>
<!--検索ボックス-->
<v-text-field v-model="key" append-icon="mdi-magnify" label="Search" single-line solo
v-on:keydown.enter="search();changePage(1)" @click:append="search();changePage(1)" >
</v-text-field>
<!--記事一覧部分-->
<v-card v-for="(inquiry) in inquiries" :key="inquiry.id" :id="inquiry.id" flat>
<v-card-text>
{{inquiry.answer}}
</v-card-text>
</v-card>
<!--ページネーション-->
<div class="text-center">
<v-pagination v-model="current_page" :length="length" @input="changePage"></v-pagination>
</div>
</div>
</template>
検索ボックスについて
<!--検索ボックス-->
<v-text-field v-model="key" append-icon="mdi-magnify" label="Search" single-line solo
v-on:keydown.enter="search();changePage(1)" @click:append="search();changePage(1)" >
</v-text-field>
今回はCSSフレームワークにveutify.jsを使っているから、固有のオプションがいくつかあるけど、
検索ボタンを押したときやエンターを押したときに、search();changePage(1)
の2つのメソッドが発火することがポイント。
この後メソッドについては書くけど、
検索メソッドを発火させつつ、ページは1ページ目に戻るよって意味。
記事一覧部分について
<!--記事一覧部分-->
<v-card v-for="(inquiry) in inquiries" :key="inquiry.id" :id="inquiry.id" flat>
<v-card-text>
{{inquiry.answer}}
</v-card-text>
</v-card>
foreach的な感じで、この後取得するレコードを回してる。
vuetify.jsだからv-forってかいてるけど、自分の環境に合わせて。
この例では、inquiriesテーブル
のレコード軍におけるanswerカラム
の内容だけを一覧しようとしている。
ページネーションについて
<!--ページネーション-->
<div class="text-center">
<v-pagination v-model="current_page" :length="length" @input="changePage"></v-pagination>
</div>
ごめん、これだけはめっちゃvuetify.jsに依存した書き方。
たまたま便利なコンポーネントがあったから楽に使えた。
自力で実装する方法もあるから、ほかのCSSフレームワーク使ってる人は、この記事も参考にしてみて↓
Laravel+Bootstrap Vueでページネーションを実装する方法
一応vuetifyで解説すると、
v-model="current_page"
としておくと、今開いているページの番号がマークされる↓
:length
は、全部で何ページあるかを指定するためにある。
@input="changePage"
は、Archive.vueが読み込まれたときに任意のページに移動してくれる・・・・みたいな感じやったかな?
ごめん忘れた。
いずれにしても、vuetify.js特有の書き方なのであんまり気にしないで。
メソッドとか
//Archive.vueのmounted部分
mounted() {
if (this.$route.query.page) {
//リロードしてもURLからページ番号を取得する
this.current_page = Number(this.$route.query.page)
} else {
//リロード後、URLにページ番号の指定がなければ1ページ目を指定
this.current_page = 1
}
if (this.$route.query.key) {
//リロードしてもURLから検索キーワードを取得する
this.key = this.$route.query.key
this.search()
} else {
//リロード後、URLにキーワードの指定がなければ全レコードを表示するメソッドを発火
//とりあえず初めに開いたらこのメソッドが走る。
this.showArchive()
}
window.addEventListener("popstate", this.handlePopstate) //ブラウザバックも任意の検索条件を適用するメソッド発火
},
主に初めに開いたときや、リロードした時に発動ってほしいことを書いている。
//Archive.vueのメソッド部分
methods: {
async showArchive() {
//キーワードなしの全レコード表示api
const result = await axios.get(`/api/inquiries?page=${this.current_page}`) //表示したいページ番号をapiに渡す
const inquiries = result.data //apiから取得した結果を格納
this.inquiries = inquiries.data //apiから取得した結果からレコード一覧を格納
this.length = inquiries.meta.last_page //総ページ数を取得(ページネーションボタン生成時に使う)
},
async search() {
const result = await axios.get('/api/inquiries/search', {
params: {
key: this.key,
page: this.current_page,
} //検索キーワードと表示したいページ番号をapiに渡す
})
this.searchKeyword = this.key //「○○」の検索結果 とか出したいので別の変数にキーワードを格納
const inquiries = result.data //apiから取得した結果を格納
this.inquiries = inquiries.data //apiから取得した検索結果のレコード一覧を格納
this.total = inquiries.meta.total //レコードの件数を取得(「〇件ヒット」)とか表示したいので
this.length = inquiries.meta.last_page //総ページ数を取得(ページネーションボタン生成時に使う)
},
handlePopstate() {//ブラウザバック、進むを検知した時に発火
this.current_page = Number(this.$route.query.page) || 1 //urlから目的のページ番号を把握
if (this.$route.query.key != undefined) {
//urlに検索キーワードの指定があれば使う
this.key = this.$route.query.key
} else {
//urlに検索キーワードの指定がなければnullっぽい感じにする
this.key = ''
}
this.search() //上記の条件がそろった状態で再検索
},
moveToTop() {
window.scroll(0, 0) //一番上までスクロール(検索やページ移動時にこれがないとスクロールバーの位置がそのままにになる)
},
changePage(number) {
//ページネーションをクリックしたときに発火するメソッド
this.current_page = number
//検索条件やページ番号の指定に合わせてURLを変化させる
let url = null
url = `${window.location.origin}/archive?page=${number}`
if (this.key) {
//検索キーワードがある場合のURLを定義
url = `${window.location.origin}/archive?key=${this.key}&page=${number}`
this.search()
} else { //検索キーワードがない場合のURLを定義
url = `${window.location.origin}/archive?page=${number}`
this.showArchive()//全レコード表示
}
//urlが変化
window.history.pushState({
number
},
`Page${number}`,
url
)
this.moveToTop()//一番上までスクロール
},
}
//Archive.vueのbeforeDestroy() 部分
beforeDestroy() {
window.removeEventListener("popstate", this.handlePopstate);
},
画面を離れるときはremoveEventListenerを忘れずに。
SPAの場合は、忘れてしまうと複数回handlePopstateが呼び出されてしまう可能性があるので、
特に注意が必要です。
Vue.js: 戻る(ブラウザバック)をハンドリングしてメソッドを呼び出す
とのこと
冒頭で箇条書きにした条件をこれで全部も網羅できてるはず。
最後に
- 絞り込み検索はどうするの?
- 複合条件検索したければ?
- 複数のカラムを対象に検索するには?
- ページ送りした時のスクロールの挙動が嫌い
- もっといいやり方があるぞ
など言いたいことはたくさんあるかもしれないが、まあこんなところでしょう。
テーブル検索とか、単純なページ送りの記事は調べたらでてきたけど、検索機能と共存させるやり方はあんまでてこんかった。
参考にしたサイト↓