やること

  • 全レコード表示用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: 戻る(ブラウザバック)をハンドリングしてメソッドを呼び出す

とのこと

冒頭で箇条書きにした条件をこれで全部も網羅できてるはず。

最後に

  • 絞り込み検索はどうするの?
  • 複合条件検索したければ?
  • 複数のカラムを対象に検索するには?
  • ページ送りした時のスクロールの挙動が嫌い
  • もっといいやり方があるぞ

など言いたいことはたくさんあるかもしれないが、まあこんなところでしょう。

テーブル検索とか、単純なページ送りの記事は調べたらでてきたけど、検索機能と共存させるやり方はあんまでてこんかった。

参考にしたサイト↓

キーイベントで特定のキーが押された場合に処理を行うv-onディレクティブのキー修飾子 [Vue.js]

無制限に質問可能なプログラミングスクール!

万が一転職できない場合は、転職保障全額返金できるコースもあり!!

無制限のメンター質問対応

 

DMMウェブキャンプでプログラミングを学習しませんか?

独学より成長スピードをブーストさせましょう!

 

まずは無料相談から!

おすすめの記事