Help us understand the problem. What is going on with this article?

【初Laravel】ワイ、POSTでページネーションを実装する

はじめに

この記事はやめ太郎氏(@Yametaro )のワイ記法を参考に書かせていただいています。
せっかくなので、僕の故郷である長崎の方言、長崎弁で綴ろうと思います。
長崎弁では自分のことを「オイ」、相手のことを「ワイ」と呼びますが、一応ワイ記法はワイを一人称としているので、例外的に本記事でもワイは自分のことを指すこととします。

ある日、大学の研究室にて

SJ教授「おい、ひろすぐ、頼みたいことがあるんだけど」

ワイ「お、ゴミ出しならやらんですよ?」

SJ教授「ゴミ出しならお前らがやらないから俺が朝やったよ。ちょっと作ってほしいwebアプリがあるの」

ワイ「webアプリ?ついに我が研究室も研究費でビジネス始めるとですか?」

SJ教授「(それはダメだろ・・・)」
SJ教授「うちの研究室の歴代の論文をデータベース化して、簡単にアブストラクトをリストで検索できるようなwebアプリを作ってほしいの、できる?」

ワイ「ほーん、余裕ですばい!まっちょってください!」

SJ教授「ただし、すでにある研究室のwebサイトにマージしたいから、バックエンドはLaravelで作ってね。」

ワイ「ら・・・ららべる・・・?そんなん知らんとですけど・・・」

SJ教授「お前、Twitterで学生エンジニアぶってるくせに、Laravel知らないんかい」
SJ教授「とりあえず、そういうことなので、頑張って調べながらやってくれや。」
SJ教授「仕様書送っとくから、よろしく〜」

ワイ「(雑すぎやろ・・・)」
ワイ「とりあえず仕様書見ながら頑張るしかなかね・・・」

Slack「スコココッ」

仕様書を見てみると

ワイ「お、仕様書が送られて来とるな、どれどれ・・・」
スクリーンショット 2020-06-15 4.40.03.png

ワイ「論文探し太郎・・・」
ワイ「勝手にこんな名前つけてたら、やめ太郎さんに怒られるばい・・・」

ワイ「ていうか、URLを綺麗にしておきたいってどういうこだわり持っとらすとかね・・・、めちゃめちゃ強調しとらすやん・・・」

ワイ「まあありがちなサイトたいね〜。これなら簡単にできそうばい」
ワイ「とりあえず、Laravelの基本的な使い方ば勉強してから、試しに作ってみよかね。」

頑張ってある程度形はできたものの

ワイ「とりあえず見た目はできた!」
ワイ「あれれ、ページネーションがうまくいかんね、なんでやろ・・・」
ワイ「2ページ目以降のリンクをクリックすると、検索キーワードがリセットされてしまう・・・」
ワイ「Laravelには、paginate()という便利なメソッドがあって、クエリにくっつけてアイテム数を指定するだけで簡単にページネーションが実現できるってQiitaに書いてあったとけどなぁ」

マトラボ先輩「POSTで投げてるからだろうね〜」

ワイ「わ!びっくりした!マトラボパイセンじゃなかですか!」
ワイ「も、もしかして、マル秘の仕様書を盗み見してたとですか・・・?」

マトラボ先輩「せっかく助けてあげようとしてたのに・・・」
マトラボ先輩「ていうか、同じ研究室なんだから別にいいでしょ」

ワイ「確かに・・・」
ワイ「師匠、助けてください・・・」

マトラボ先輩「良かろう。」
マトラボ先輩「2ページ目以降で検索条件がリセットされちゃうのは、POSTでキーワードを投げているので、URLでその値が保持できてないんだね」
マトラボ先輩「そもそも、最初の検索するときの通信はPOSTなのに、ページリンク押したらGETで通信するんだから、それなんとかしないとね」

ワイ「POSTだと無理とですか?」

マトラボ先輩「うーん、pagenate()単体だと難しいかも。」
マトラボ先輩「ほら、GETでのリクエストだと、URLに?hoge=fugaみたいな感じで、検索キーワードを持っておけるでしょ?」
マトラボ先輩「こうしておけば、今のページ番号検索キーワードをパラメータとして持たせておいて、ページネーションが実現できるんだけど・・・」
マトラボ先輩「POSTでのリクエストは、URLには何もつかないからね」
マトラボ先輩「調べる限りLaravelのpaginate()GETでのリクエストにしか対応していないみたいだし」

ワイ「そ、そんなぁ・・・」
ワイ「教授に仕様変更願を出さんばですね、これはワイには不可能ですばい」
ワイ「わ、ワイのせいじゃなかですよ!Laravelが悪かとです!バーカバーカ!」

マトラボ先輩「(こいつ、ガキかよ・・・)」
マトラボ先輩「せっかくだし、ページネーション実装してみなよ。POSTでもできるはずだよ。」

ワイ「じ、自分で実装・・・?」

マトラボ先輩「何事も勉強!ページネーションなんて、webエンジニアの9割は10分で実装できると言われているんだから、ひろすぐ君もできるようにならなきゃ!」

ワイ「9割が10分で実装できる・・・?この業界は天才しかおらんとですか・・・?」
ワイ「パイセン、手取り足取り教えてください!」

マトラボ先輩「あ、いや、僕は研究があるから!自分で頑張ってね〜、バイバイ」

ワイ「えええええええ!」
ワイ「助けてくれんとですか・・・、トホホ」

ページネーションの実装と言われても

ワイ「そもそもページネーションってどんな仕組みで動いとるとかね」
ワイ「とりあえずググってまとめてみるばい!」

ページング機能を実装する上で必要な情報は以下の通り
1. 1ページあたり何件表示するか(limit)
2. 今何ページ目にいるのか
3. そのページはデータの何番目から始まるのか(offset)
4. 全部で何件あるのか
5. 全部で何ページあるのか

SQLのクエリでは、offsetとlimitを使った検索方法があって、部分的に結果を返すことが可能

ワイ、完全に理解する

ワイ「ふむふむ、なんとなく分かってきたばい」
ワイ「つまり、こういうことたい」
ワイ「部分的な結果を表示させたい時は、検索キーワード、offset、limitで検索をかける。」
ワイ「ページリンクを押した時に、offset、limitに加えて検索キーワードを再送信することで、欲しいページの内容をリクエストして、再描画すればよかったい!」
ワイ「今日は冴えとる!!」

実際にコードを書いてみる

ワイ「取りあえず、テンプレート側から書こうかね。」
ワイ「まずは、値を持たせておく機構を作るばい」

ronbun.blade.php
<form action="{{ url('/')}}" method="POST" name="search_form" id="search-form">
  @csrf
  <input type="hidden" id="in-offset" name="offset" value="{{$offset}}"> //オフセット
  <input type="hidden" id="in-limit" name="limit" value="{{$limit}}"> //リミット
  <input type="hidden" id="in-key" name="key" value="{{$keyword}}"> //検索キーワード

  <input type="text" name="keyword" placeholder="キーワード" value="{{$keyword}}">
  <button type="submit" id="post">検索</button>
</form>

ワイ「こんな感じで、hiddenにそれぞれの値を持たせておけば、リンクを押した時にvalueを書き換えて送信できるばい」
ワイ「よし、次は、リンクの部分を作っていくばい」
ワイ「デザインはめんどくさいからBootstrapくんになんとかしてもらおう・・・」

ronbun.blade.php
<nav aria-label="Page navitation" class="but-phone">
  <ul class="pagination justify-content-center">
  @if($count == 0) 
    <div>そんな研究ありませ</div>
  @else
    //「最初に戻る」、「前へ」ボタン
    <li class="page-item nonactive toFirst"><a class="page-link first" href="#">«</a></li>
    <li class="page-item nonactive toPrev"><a class="page-link prev" href="#"></a></li>

    //ページの数分ページリンクを生成                         
    @for($i = 0; $i < pageNum; $i ++)
      //カレントページならリンクに色をつける
      @if($i == $pageNumber)
        <li class="page-item active page" data-offset="{{$i*10}}"><a class="page-link link-num" href="#">{{$i+1}}</a></li>
      @else
        <li class="page-item nonactive page" data-offset="{{$i*10}}"><a class="page-link link-num" href="#">{{$i+1}}</a></li>
      @endif
    @endfor

    //「最後に飛ぶ」、「次へ」ボタン
    <li class="page-item nonactive toNext"><a class="page-link next" href="#"></a></li>
    <li class="page-item nonactive toEnd"><a class="page-link end" href="#">»</a></li>
  @endif
  </ul>
</nav>

ワイ「まずは”前へ”ボタンと、”最初に戻る”ボタンを作って・・・」
ワイ「そこからページリンクをページ数分回して作る」
ワイ「カレントのページリンクには色をつけたいから、classactiveにして、そうでないものはnonactiveにしちょる」
ワイ「最後に、”次へ”ボタンと、”最後へ飛ぶ”ボタンを作って完成やね」
ワイ「data-offset自分のページの時のoffsetを持たせておけば、クリックされた時にその値を送信すればよかけん、楽やね」
ワイ「さて、そしたら次は送信する機構を作るばい」
ワイ「リンクを押したらhiddenを書き換えて送信したいから、JavaScriptで書けばよかね」
ワイ「jQuery、苦手やけど、練習やと思って頑張るばい」

script.js
$(function () { 
  //必要な値の取得
  const offset = $('#in-offset').val();
  const limit = $('#in-limit').val();
  const pages = ???

  $(document).on('click', '#post', function () {
    //検索ボタンを押した時
    $('#in-offset').val(0);
  }).on('click', '.page', function () {
    //ページリンクを押した時
    const pageoffset = $(this).attr('data-offset');
    $('#in-offset').val(pageoffset);
    $('#search-form').submit();
  }).on('click', '.toFirst', function () {
    //「最初に飛ぶ」ボタンを押した時
    $('#in-offset').val(0);
    $('#search-form').submit();
  }).on('click', '.toPrev', function () {
    //「前へ」ボタンを押した時
    $('#in-offset').val(Number(offset) - Number(limit));
    $('#search-form').submit();
  }).on('click', '.toNext', function () {
    //「次へ」ボタンを押した時
    $('#in-offset').val(Number(offset) + Number(limit));
    $('#search-form').submit();
  }).on('click', '.toEnd', function () {
    //「最後に飛ぶ」ボタンを押した時
    $('#in-offset').val((Number(pages)-1)*Number(limit));
    $('#search-form').submit();
  });
});

ワイ「作りよって思ったけど・・・」
ワイ「これ、ページの総数がないと最後に飛ぶの計算ができんたい」
ワイ「JSで取ってきたいけん、hidden追加するばい」
ワイ「なんか、あとでヒット件数も使うってワイの勘が言っとる・・・」
ワイ「今は使わないけど、とりあえず追加しとこ・・・」

ronbun.blade.php
<form action="{{ url('/')}}" method="POST" name="search_form" id="search-form">
  @csrf
  <input type="hidden" id="in-offset" name="offset" value="{{$offset}}"> //オフセット
  <input type="hidden" id="in-limit" name="limit" value="{{$limit}}"> //リミット
  <input type="hidden" id="in-key" name="key" value="{{$keyword}}"> //検索キーワード
  <input type="hidden" id="in-pages" name="pages" value="{{$pages}}"> //ページ数
  <input type="hidden" id="in-count" name="count" value="{{$count}}"> //ヒット件数

  <input type="text" name="keyword" placeholder="キーワード" value="{{$keyword}}">
  <button type="submit" id="post">検索</button>
</form>

ワイ「これでJSは完成ばい」

script.js
$(function () { 
  //必要な値の取得
  const offset = $('#in-offset').val();
  const limit = $('#in-limit').val();
  const pages = $('#in-pages').val();
  const count = $('#in-count').val(); //あとで使う予感がする

  $(document).on('click', '#post', function () {
    //検索ボタンを押した時
    $('#in-offset').val(0);
  }).on('click', '.page', function () {
    //ページリンクを押した時
    const pageoffset = $(this).attr('data-offset');
    $('#in-offset').val(pageoffset);
    $('#search-form').submit();
  }).on('click', '.toFirst', function () {
    //「最初に飛ぶ」ボタンを押した時
    $('#in-offset').val(0);
    $('#search-form').submit();
  }).on('click', '.toPrev', function () {
    //「前へ」ボタンを押した時
    $('#in-offset').val(Number(offset) - Number(limit));
    $('#search-form').submit();
  }).on('click', '.toNext', function () {
    //「次へ」ボタンを押した時
    $('#in-offset').val(Number(offset) + Number(limit));
    $('#search-form').submit();
  }).on('click', '.toEnd', function () {
    //「最後に飛ぶ」ボタンを押した時
    $('#in-offset').val((Number(pages)-1)*Number(limit));
    $('#search-form').submit();
  });
});

ワイ「#postのクリックは、検索ボタンのことやね」
ワイ「これは、どのページからでも新しいキーワードで正しく検索できるうにしとるとばい」
ワイ「こればせんと、そのままカレントのoffsetが送信されてしまって、1ページ目から表示できない可能性があるけんね」
ワイ「そのあとからは、ページリンクや、矢印リンクを押した時に、正しいoffsetをセットして、サーバーに送信しとるったいね」

ワイ「さてさて、最後は検索する機構ば作らんばね」
ワイ「これは、コントローラーに書けばよかね」

RonbunController.php
//1ページあたりに表示する件数
const LIMIT = 10;

  public function search(Request $request){
    //キーワード取得
    $keyword = $request->input('keyword');

    //ヒット件数取得
    $count = Ronbun::where('title', 'LIKE', "%$keyword%")
    ->orWhere('author', 'LIKE', "%$keyword%")
    ->orWhere('predation', 'LIKE', "%$keyword%")
    ->count();

    //オフセット取得
    $offset = (int)$request->input('offset');
    if(!isset($offset) || ($offset < 0 && $offset > $count )){
      $offset = (int)0;
    }

    //ページ数計算
    $pages = ceil($count / self::LIMIT);

    //現ページ番号計算
    $pageNumber = $offset / self::LIMIT;

    //検索
    $sql = Ronbun::where('title', 'LIKE', "%$keyword%")
    ->orWhere('author', 'LIKE', "%$keyword%")
    ->orWhere('predation', 'LIKE', "%$keyword%")
    ->offset($offset)
    ->limit(self::LIMIT)
    ->get();

    //テンプレートにデータを渡す
    return view('plist', ['data'=> $sql, 'keyword'=>$keyword, 'offset'=>$offset, 'pages'=>$pages, 'pageNumber'=>$pageNumber, 'count'=>$count, 'limit'=>self::LIMIT]);
  }

ワイ「よっしゃ!書けたばい!」
ワイ「動くかな??」

動かしてみると

ワイ「あれれ、リンクを押すと、ページの先頭にスクロールするだけやん」
ワイ「なんで・・・?」
ワイ「うーん、リンクの遷移先が#やけんかなあ」
ワイ「でも、そうしておかんばなあ」
ワイ「そもそもカーソル当ててもポインタにならんたいね」
ワイ「CSSでなんとかなるやろ」

style.css
/*リンクの無効化*/
.page-link{
  pointer-events: none;
}

/*ホバー時にポインタにする*/
.page-item{
  cursor : pointer;
}

/*ホバー時に色を変える*/
.nonactive:hover  .page-link{
  background-color: #dee2e6;
  border-color: #dee2e6;
}

ワイ「おっし、これでリンクは無効にできたばい」
ワイ「ちゃんとカーソル当てたらポインタになるし、ついてにホバーした時色変わるお洒落仕様にしたったけんね」

2度目の挑戦

ワイ「ちゃんとできるかな・・・(ドキドキ)」
ワイ「ポチっとな」

ワイ「できた!!!
ワイ「ついにできたで!!」
ワイ「あれれ」
ワイ「これ、1ページ目の時でも"戻る"ボタンが押せてしまうばい」
ワイ「もしかして・・・」
ワイ「やっぱり!最後のページの時にも”次へ”ボタンが押せてしまうたい!」
ワイ「確か、Bootstrapくんは、classdisableを追加してやれば押せなくできたはず・・・」
ワイ「ページ読み込み時にカレントのページ番号を確認して、classばいじるか・・・」

ここで勘が的中

ワイ「はっっっ・・・!」
ワイ「ここでさっきのcountば使うったい!」
ワイ「なんちゅう勘・・・」

script.js
//矢印リンクの非活性化
//カレントが1ページ目の時
if (Number(offset) == 0) {
  $('.toFirst').addClass('disabled');
  $('.toPrev').addClass('disabled');
} else {
  $('.toFirst').removeClass('disabled');
  $('.toPrev').removeClass('disabled');
}
//カレントが最後のページの時
if (Number(offset) == (Math.ceil(count / limit) - 1)* Number(limit)){
  $('.toEnd').addClass('disabled');
  $('.toNext').addClass('disabled');
} else {
  $('.toEnd').removeClass('disabled');
  $('.toNext').removeClass('disabled');
}
style.css
/*矢印リンクの非活性化*/
.disabled{
  pointer-events: none;
}

ワイ「もうちょいいい書き方あったかもしれんけど・・・汗」
ワイ「とりあえず、カレントページのoffsetを確認して、最初のページと最後のページの時は矢印リンクを使えんくしてみたばい」

何はともあれ完成

ワイ「ついに完成したばい・・・涙」

SJ教授「お、ひろすぐ、もうできたのか」
SJ教授「Laravelの勉強にはなったかい」

ワイ「めちゃめちゃ疲れましたよ・・・」
ワイ「後半はLaravelの勉強というよりページネーションの勉強でしたし・・・」

SJ教授「ページネーション?そんなのメソッドに渡して終わりでしょ?」

ワイ「ふふん、教授、知らんとですか?」
ワイ「Laravelのpaginate()メソッドでは、POSTリクエストでのページネーションはできんとですばい!」
ワイ「全てワイが独自に実装したとですばい!」

SJ教授「あー・・・、そのことなんだけどさ・・・」

ワイ「え・・・?」

SJ教授「POSTのページネーション、去年マトラボくんが実装してくれてるから、どっか探せばメソッド出てくるはずだよ」

ワイ「へ?」
ワイ「すでにあった・・・だと・・・?」
ワイ「ワイの努力・・・」

マトラボ先輩は隣の部屋で

マトラボ先輩「(ふふっ、後輩を育てるのが先輩の仕事だからね・・・!)」

〜おわり〜

HirosuguTakeshita
WEBと画像処理(超解像)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした