はじめに
この記事はやめ太郎氏(@Yametaro )のワイ記法を参考に書かせていただいています。
せっかくなので、僕の故郷である長崎の方言、長崎弁で綴ろうと思います。
長崎弁では自分のことを「オイ」、相手のことを「ワイ」と呼びますが、一応ワイ記法はワイを一人称としているので、例外的に本記事でもワイは自分のことを指すこととします。
ある日、大学の研究室にて
SJ教授「おい、ひろすぐ、頼みたいことがあるんだけど」
ワイ「お、ゴミ出しならやらんですよ?」
SJ教授「ゴミ出しならお前らがやらないから俺が朝やったよ。ちょっと作ってほしいwebアプリがあるの」
ワイ「webアプリ?ついに我が研究室も研究費でビジネス始めるとですか?」
SJ教授「(それはダメだろ・・・)」
SJ教授「うちの研究室の歴代の論文をデータベース化して、簡単にアブストラクトをリストで検索できるようなwebアプリを作ってほしいの、できる?」
ワイ「ほーん、余裕ですばい!まっちょってください!」
SJ教授「ただし、すでにある研究室のwebサイトにマージしたいから、バックエンドはLaravelで作ってね。」
ワイ「ら・・・ららべる・・・?そんなん知らんとですけど・・・」
SJ教授「お前、Twitterで学生エンジニアぶってるくせに、Laravel知らないんかい」
SJ教授「とりあえず、そういうことなので、頑張って調べながらやってくれや。」
SJ教授「仕様書送っとくから、よろしく〜」
ワイ「(雑すぎやろ・・・)」
ワイ「とりあえず仕様書見ながら頑張るしかなかね・・・」
Slack「スコココッ」
仕様書を見てみると
ワイ「論文探し太郎・・・」
ワイ「勝手にこんな名前つけてたら、やめ太郎さんに怒られるばい・・・」
ワイ「ていうか、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ページあたり何件表示するか(limit)
- 今何ページ目にいるのか
- そのページはデータの何番目から始まるのか(offset)
- 全部で何件あるのか
- 全部で何ページあるのか
SQLのクエリでは、offsetとlimitを使った検索方法があって、部分的に結果を返すことが可能
ワイ、完全に理解する
ワイ「ふむふむ、なんとなく分かってきたばい」
ワイ「つまり、こういうことたい」
ワイ「部分的な結果を表示させたい時は、検索キーワード、offset、limitで検索をかける。」
ワイ「ページリンクを押した時に、offset、limitに加えて検索キーワードを再送信することで、欲しいページの内容をリクエストして、再描画すればよかったい!」
ワイ「今日は冴えとる!!」
実際にコードを書いてみる
ワイ「取りあえず、テンプレート側から書こうかね。」
ワイ「まずは、値を持たせておく機構を作るばい」
<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くんになんとかしてもらおう・・・」
<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>
ワイ「まずは**”前へ”ボタンと、”最初に戻る”ボタンを作って・・・」
ワイ「そこからページリンクをページ数分回して作る」
ワイ「カレントのページリンクには色をつけたいから、class
ばactive
にして、そうでないものはnonactive
にしちょる」
ワイ「最後に、”次へ”ボタンと、”最後へ飛ぶ”ボタンを作って完成やね」
ワイ「data-offset
に自分のページの時のoffsetを持たせておけば**、クリックされた時にその値を送信すればよかけん、楽やね」
ワイ「さて、そしたら次は送信する機構を作るばい」
ワイ「リンクを押したら**hidden
を書き換えて送信**したいから、JavaScriptで書けばよかね」
ワイ「jQuery、苦手やけど、練習やと思って頑張るばい」
$(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
に追加するばい」
ワイ「なんか、あとでヒット件数も使うってワイの勘が言っとる・・・」
ワイ「今は使わないけど、とりあえず追加しとこ・・・」
<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は完成ばい」
$(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をセットして、サーバーに送信しとるったいね」
ワイ「さてさて、最後は検索する機構ば作らんばね」
ワイ「これは、コントローラーに書けばよかね」
//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でなんとかなるやろ」
/*リンクの無効化*/
.page-link{
pointer-events: none;
}
/*ホバー時にポインタにする*/
.page-item{
cursor : pointer;
}
/*ホバー時に色を変える*/
.nonactive:hover .page-link{
background-color: #dee2e6;
border-color: #dee2e6;
}
ワイ「おっし、これでリンクは無効にできたばい」
ワイ「ちゃんとカーソル当てたらポインタになるし、ついてにホバーした時色変わるお洒落仕様にしたったけんね」
2度目の挑戦
ワイ「ちゃんとできるかな・・・(ドキドキ)」
ワイ「ポチっとな」
ワイ「できた!!!」
ワイ「ついにできたで!!」
ワイ「あれれ」
ワイ「これ、1ページ目の時でも**"戻る"ボタンが押せてしまうばい」
ワイ「もしかして・・・」
ワイ「やっぱり!最後のページの時にも”次へ”ボタンが押せてしまうたい!」
ワイ「確か、Bootstrapくんは、class
にdisable
を追加してやれば押せなくできたはず・・・」
ワイ「ページ読み込み時にカレントのページ番号を確認**して、class
ばいじるか・・・」
ここで勘が的中
ワイ「はっっっ・・・!」
ワイ「ここでさっきのcount
ば使うったい!」
ワイ「なんちゅう勘・・・」
//矢印リンクの非活性化
//カレントが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');
}
/*矢印リンクの非活性化*/
.disabled{
pointer-events: none;
}
ワイ「もうちょいいい書き方あったかもしれんけど・・・汗」
ワイ「とりあえず、カレントページのoffsetを確認して、最初のページと最後のページの時は矢印リンクを使えんくしてみたばい」
何はともあれ完成
ワイ「ついに完成したばい・・・涙」
SJ教授「お、ひろすぐ、もうできたのか」
SJ教授「Laravelの勉強にはなったかい」
ワイ「めちゃめちゃ疲れましたよ・・・」
ワイ「後半はLaravelの勉強というよりページネーションの勉強でしたし・・・」
SJ教授「ページネーション?そんなのメソッドに渡して終わりでしょ?」
ワイ「ふふん、教授、知らんとですか?」
ワイ「Laravelのpaginate()
メソッドでは、POSTリクエストでのページネーションはできんとですばい!」
ワイ「全てワイが独自に実装したとですばい!」
SJ教授「あー・・・、そのことなんだけどさ・・・」
ワイ「え・・・?」
SJ教授「POSTのページネーション、去年マトラボくんが実装してくれてるから、どっか探せばメソッド出てくるはずだよ」
ワイ「へ?」
ワイ「すでにあった・・・だと・・・?」
ワイ「ワイの努力・・・」
マトラボ先輩は隣の部屋で
マトラボ先輩「(ふふっ、後輩を育てるのが先輩の仕事だからね・・・!)」
〜おわり〜