LaravelとVue.jsを使って複数条件の検索を実装するぜ
- type(機種)
- key(キーワード)
の2つの複合検索を想定。
type(機種)はInquiryテーブルのtypeカラム を検索対象とし、
key(キーワード)はInquiryテーブルの
answerカラム と
questionカラム の
両方
を検索対象とする。
また、検索を行うページのURLは
localhost:8080/inquiries
環境
- vue 2
- vuetify2
- vue-router
モデルと「〇〇での検索結果」という表示のための変数を作る
data() {
inquiries: [] //検索結果をapiから格納
detailSearchKey: {
//リロード時にクエリを再認識できるようにURLからパラメーターを取得
type: this.$route.query.type,//画像①同じ値が入る
key: this.$route.query.key,//画像の②同じ値が入る
},
current_page: 1 //ページ番号初期値
//「〇〇での検索結果」と表示するための変数
searchKeyword: "",//画像の③の部分
searchType: "",//画像の③の部分
},
//ヒットした記事の合計件数
total:"",//画像④の部分に表示
フォームを作る
<template>
<v-text-field v-model="detailSearchKey.type" label="機種名" >
</v-text-field><!--画像①の箇所-->
<v-text-field v-model="detailSearchKey.key" label="キーワード">
</v-text-field><!--画像②の箇所-->
<v-btn @click="
searchDetail();
changePageDetailSearch(1);
">検索</v-btn>
</template>
<!--画像③の箇所-->
<p>
<span v-if="searchType"> 機種:{{ searchType }}</span>
<span v-if="searchKeyword"> キーワード:{{ searchKeyword }}</span>
</p>
<!--画像④の箇所-->
<p
v-if="searchKeyword || searchType">
の検索結果 {{ total }}件ヒット
</p>
<p v-else>全件表示</p>
//記事一覧
<v-card v-for="inquiry in inquiries" :key="inquiry.id">
<div>機種:{{inquiry.type}} </div>
<div>問い合わせ内容:{{inquiry.question}}</div>
<div>解答内容:{{inquiry.answer}}</div>
</v-card>
//ページネーション
<v-pagination v-model="current_page" :length="length" @input="changePageDetailSearch"></v-pagination>
v-text-field
は、CSSフレームワークvuetifyの書き方。
input
とかに置き換えてもいい。
検索ボタンを押すと、
検索メソッド(searchDetail()
)が走って、検索結果の1ページ目にジャンプする(changePageDetailSearch(1)
)
変数searchType
やsearchKeyword
には、検索条件を示す文字が入る。
この部分↓
機種:{{ searchType }} キーワード:{{ searchKeyword }}
にモデル(detailSearchKey.typeやdetailSearchKey.key
)を指定してしまうと、
まだ検索を実行していないのにフォームに入力した内容がリアルタイムに表示されてしまうので分けている。
v-pagination
タグもvuetify独自の書き方なので、自分の環境に合わせて書いてほしい。
lengthにはページネーションの総ページ数
totalにはレコード数が格納される手筈にしていく。
では、メソッドの中身を説明するぜ。
メソッド
メソッドごとに分けて説明するけど、全部methods(){ }
の中に定義する。
searchDetail()メソッド
async searchDetail() {
this.resetDetailSearchDisplay()//検索条件の文字列をリセットする関数(次の見出しで解説)
const result = await axios.get("/api/inquiries/searchDetail", {
params: {
detailSearchKey: this.detailSearchKey,
page: this.current_page,
},
});
//この条件で検索したよ!って文字列で表示する用の変数
if (this.detailSearchKey.type != null) {
this.searchType = this.detailSearchKey.type
}
if (this.detailSearchKey.key != null) {
this.searchKeyword = this.detailSearchKey.key
}
//検索結果取得
const inquiries = result.data;
this.inquiries = inquiries.data;
//検索にひっかかったレコードの数など取得して
this.total = inquiries.meta.total;
this.length = inquiries.meta.last_page; //総ページ数を取得
},
検索メソッドが発火するタイミングで、「○○の検索結果」という条件が表示されていたらリセットする。(resetDetailSearchDisplay()
)
検索用APIに検索条件と、ページ番号を投げている。
data()
で指定した通り、通常は1
が投げられる。
resetDetailSearchDisplay()メソッド
resetDetailSearchDisplay() {
this.searchKeyword = ""
this.searchType = ""
},
「今こんな条件で検索した結果を表示しているよ!」という説明文を
検索のたびにいったんリセットするメソッド。
これがないと、2回目以降検索した時に
1回目に検索した条件とごちゃ混ぜになってしまう。
changePageDetailSearch()メソッド
検索ボタンを押したときに、検索メソッドと同時に呼び出してたメソッド。
<v-btn @click="searchDetail();changePageDetailSearch(1);">検索</v-btn>
changePageDetailSearch(number) {
//引数はジャンプしたい先のページ番号
this.current_page = number;
let url = null;
//検索条件に合わせてURLが変化する仕組みを作る
url = `${window.location.origin}/inquiries?detailsearchpage=${number}`;//ベースとなるURL
if (this.detailSearchKey.type) {
//機種名が検索条件に存在すればURLに追記
url += "&type=" + this.detailSearchKey.type;
}
if (this.detailSearchKey.key) {
//キーワードが検索条件に存在すればURLに追記
url += "&key=" + this.detailSearchKey.key;
}
//検索メソッド発動
this.searchDetail(); //詳細検索メソッドも走らせないとURLが変わるだけになる
//URLを変化させる。
window.history.pushState({
number,
},
`Page${number}`,
url
);
this.moveToTop();//1番上までスクロールして戻る。(次の見出しで解説)
},
例えば
- 機種がiphone14
- キーワードが「あ」
の場合は、
http://localhost/inquiries?detailsearchpage=1&type=iphone14&key=あ
のようなURLになる。
URLを変化させる理由は、リロードされた場合などにも同じ条件で検索し直してほしいから。
URL上の
type=iphone14
key=あ
detailsearchpage=1
などから、値を取得してモデルに格納する処理を後で書く。
moveToTop()メソッド
さっきのchangePageDetailSearch(number)
の最後に登場する関数。
moveToTop() {
window.scroll(0, 0);
},
単純にページ最上部に強制スクロールするだけ。
これを書かないと、ページは切り替わるけどスクロールバーの位置がそのままになってしまう。(下までスクロールした状態の次のページが表示されるみたいな)
searchDetailHandlePopstate()メソッド
searchDetailHandlePopstate() {
this.current_page = Number(this.$route.query.detailsearchpage) || 1;
}
リロードやブラウザバックした時に、ページ番号が迷子にならないよに発動させるメソッド。
後でmountedで呼び出す。
mounted
//URLに検索条件が指定されていれば使う
this.$route.query.key ? this.detailSearchKey.key = this.$route.query.key : this.detailSearchKey.key=""
this.$route.query.type ? this.detailSearchKey.type = this.$route.query.type : this.detailSearchKey.type=""
this.searchDetail();
window.addEventListener("popstate", this.searchDetailHandlePopstate); //ブラウザバックも迷子にならないやつ
ページが読み込まれた時に検索が走る様にする。
検索条件を指定した状態でリロードやブラウザバックを行っても大丈夫な様に書いてる。
ブラウザバックしても、取得結果がおかしくならないようにする処理。
バックエンド(Laravel)
api.php
<?php
use App\Http\Controllers\InquiryController; //使用するコントローラーを追記
//中略
Route::get('/inquiries/searchDetail', [InquiryController::class, 'searchDetail']);
ドメイン/api/inquiries/searchDetail
がvueから呼び出されたら、検索条件に応じた検索結果を返すAPIを呼び出す。
コントローラー
//backend/app/Http/Controllers/InquiryController.php
<?php
namespace App\Http\Controllers;
use App\Models\Inquiry;
use Illuminate\Http\Request;
//中略
public function search(Request $request){
$requestKey = json_decode($request->detailSearchKey);//受け取ったリクエストをオブジェク形式に変換(そのままだと文字列として扱われてしまう)
$inquiry = Inquiry::query();
$pat = '%' . addcslashes($requestKey->key, '%_\\') . '%';//キーワードは部分一致でいいことにする
if($requestKey->key){
$inquiry->where(function ($query) use($pat) {
$query
->where('question','LIKE',$pat)
->orWhere('answer','LIKE',$pat)
;
});
}
if($requestKey->type ){
//機種名
$inquiry->where(function ($query) use($requestKey){
$query->where('type',$requestKey->type);//完全一致
});
}
$inquiry->orderBy('created_at', 'desc')->paginate(20);//新しい順に1ページ当たり20件ずつ表示
}
指定しなかった箇所は無条件検索になる。
type(機種)もkey(キーワード)も未入力なら全部表示する
キーワードしか入力がないなら、機種は問わずにanswerカラムかquestionカラムに該当内容があれば表示する。
タイプしかなければ、キーワードは無視して機種が一致するものを全て表示する。