Laravel pagination resource helper for vue custom pagination component with store.
php artisan make:model Gif -arR<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PaginationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage(),
'query' => $request->query(),
];
}
}<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class GifCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'paginate' => new PaginationResource($this),
];
// return parent::toArray($request);
}
}In Controller just return in index() new GifCollection($q->paginate($perpage)); collection for Route::resource('gifs', GifController::class);.
<?php
namespace App\Http\Controllers;
use App\Models\Gif;
use App\Http\Requests\StoreGifRequest;
use App\Http\Requests\UpdateGifRequest;
use App\Http\Resources\GifCollection;
use App\Http\Resources\GifResource;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class GifController extends Controller
{
protected $allow_orderby = ['id', 'name', 'created_at'];
/**
* Display a listing of the resource.
*/
public function index()
{
Gate::authorize('viewAny', Gif::class);
$q = Gif::query();
$perpage = request()->integer('perpage', default: config('default.panel_perpage', 12));
$orderby = request()->input('orderby', default: 'id');
if (!in_array($orderby, $this->allow_orderby)) {
$orderby = 'id';
}
if (request()->filled('sortpage')) {
request()->query('sortpage') == 'asc' ? $q->oldest($orderby) : $q->latest($orderby);
}
if (request()->filled('search')) {
// Filter here if you want
$str = request()->input('search');
// Case insensitive
$query->orWhere($name, 'LIKE', "%{$str}%");
// Case sensitive
// $query->orWhere($name, 'like', "%{$str}%");
}
return new GifCollection($q->paginate($perpage));
}
}import { ref, computed, nextTick } from 'vue';
import { defineStore, storeToRefs } from 'pinia';
import { useRoute } from 'vue-router';
import router from '@/router';
import axios from 'axios';
export const useItemStore = defineStore('gifs', () => {
// State
const route = useRoute();
const error = ref(false);
const message = ref(null);
const item = ref({});
const last_item = ref({});
let sort_page = ref('desc');
let orderby_page = ref('id');
let current_page = ref(1);
let last_page = ref(1);
let perpage = ref(5);
let search = ref('');
let list = ref([]);
// Getters, Setters
const getItem = computed(() => item);
// With value
const getError = computed(() => error.value);
const getMessage = computed(() => message.value);
const setOrderBy = computed({
get: () => {
return orderby_page.value;
},
set: async (v) => {
orderby_page.value = v;
await loadList();
},
});
const setSortBy = computed({
get: () => {
return sort_page.value;
},
set: async (v) => {
sort_page.value = v;
await loadList();
},
});
const setSearch = computed({
get: () => {
return search.value;
},
set: async (v) => {
search.value = v;
await loadList();
},
});
const setPage = computed({
get: () => {
return current_page.value;
},
set: async (v) => {
current_page.value = v;
await loadList();
},
});
const setPerPage = computed({
get: () => {
return perpage.value;
},
set: async (v) => {
perpage.value = v;
await loadList();
},
});
// Actions
function urlQuery() {
const query = new URLSearchParams();
query.append('page', current_page.value);
query.append('perpage', perpage.value);
query.append('orderby', orderby_page.value);
query.append('sortpage', sort_page.value);
query.append('search', search.value);
return query.toString();
}
async function loadList() {
let dontScroll = 0;
search.value.length > 0 ? (dontScroll = 1) : (dontScroll = 0);
let res = await axios.get('/api/admin/gifs?' + urlQuery());
list.value = res?.data?.data ?? [];
// From PaginationResource
last_page.value = res?.data.paginate.total_pages ?? 1;
current_page.value = res?.data.paginate.current_page ?? 1;
orderby_page.value = res?.data.paginate.query.orderby ?? 'id';
sort_page.value = res?.data.paginate.query.sortpage ?? 'desc';
if (current_page.value > last_page.value) {
current_page.value = last_page.value;
}
router.push({
query: {
page: current_page.value,
perpage: perpage.value,
sortpage: sort_page.value,
orderby: orderby_page.value,
},
params: { savePosition: dontScroll },
});
}
async function deleteItem(id) {
try {
if (confirm('Delete ?')) {
let res = await axios.delete('/api/admin/gifs/' + id);
setMessage(res);
await loadList();
const row = document.querySelector('.list-item' + id);
if (row) {
row.remove();
}
}
} catch (err) {
setError(err);
}
scrollTop();
}
async function createItem(e) {
try {
let res = await axios.post('/api/admin/gifs', new FormData(e.target));
setMessage(res);
resetForm(e);
lastItem();
} catch (err) {
setError(err);
}
scrollTop();
}
async function updateItem(id, data) {
try {
data.append('_method', 'PATCH');
let res = await axios.post('/api/admin/gifs/' + id, data);
setMessage(res);
} catch (err) {
setError(err);
}
loadItem(id);
scrollTop();
}
async function loadItem(id) {
try {
let res = await axios.get('/api/admin/gifs/' + id);
item.value = res?.data.data;
} catch (err) {
setError(err);
}
}
async function removeImage(id) {
try {
let res = await axios.get('/api/admin/gifs/remove/' + id);
setMessage(res);
loadItem(id);
} catch (err) {
setError(err);
}
}
async function lastItem() {
try {
let res = await axios.get('/api/admin/gifs/last');
last_item.value = res?.data?.data;
} catch (err) {
setError(err);
}
}
function setPerpage(value) {
if (value < 5) {
perpage.value = 5;
}
loadList();
}
function prevPage() {
current_page.value--;
if (current_page.value <= 0) {
current_page.value = 1;
}
loadList();
}
function nextPage() {
current_page.value++;
if (current_page.value >= last_page.value) {
current_page.value = last_page.value;
}
loadList();
}
function resetForm(e) {
e.target.reset();
}
function clearError() {
message.value = null;
error.value = false;
}
function setMessage(res) {
message.value = res?.data?.message;
error.value = false;
}
function setError(err) {
message.value = err?.response?.data?.error ?? err?.message ?? 'Ups! Invalid data.';
error.value = true;
}
function updateMessage(msg) {
message.value = msg;
}
function updateError(err = true) {
error.value = err;
}
function scrollTop() {
// Need html tag with overflow-y: scroll
window.scrollTo({ top: 1, behavior: 'smooth' });
// document.querySelector(id).scrollIntoView({behavior: 'smooth' });
}
return {
setPerPage,
setOrderBy,
setSortBy,
setSearch,
setPage,
message,
error,
item,
last_item,
search,
current_page,
orderby_page,
sort_page,
last_page,
perpage,
list,
getError,
getMessage,
getItem,
lastItem,
setMessage,
setError,
urlQuery,
resetForm,
clearError,
scrollTop,
updateMessage,
updateError,
removeImage,
createItem,
loadItem,
updateItem,
loadList,
deleteItem,
setPerpage,
prevPage,
nextPage,
};
});<script setup>
import PaginateCustom from '@/components/utils/pagination/PaginateCustom.vue';
import NoRecords from '@/components/utils/alerts/NoRecords.vue';
import ErrorMessage from '@/components/utils/alerts/ErrorMessage.vue';
import { useItemStore } from '@/stores/gifs.js';
import { onBeforeMount, onMounted, ref, watch } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
const store = useItemStore();
const route = useRoute();
const httphost = ref(null);
onBeforeMount(async () => {
store.clearError();
// Update from browser query
store.perpage = route.query.perpage ?? 5;
store.current_page = route.query.page ?? 1;
store.sort_page = route.query.sortpage ?? 'desc';
await store.loadList();
});
watch(
() => route.query.page,
async (newId, oldId) => {
store.current_page = route.query.page ?? 1;
await store.loadList();
}
);
onMounted(() => {
httphost.value = window.location.origin;
});
</script>
<template>
<div class="items">
<div class="item" :class="'list-item' + i.id" v-for="i in store.list">
<div class="field">{{ i.id }}</div>
<div class="field"><img :src="'/img/gif?path=' + i.image" class="field-gif" onerror="this.src='/default/default.gif'" /></div>
<div class="field">{{ i.name }}</div>
<div class="field">{{ i.image }}</div>
<div class="field">{{ i.created_at }}</div>
<div class="field field-last">
<RouterLink :to="'/admin/gifs/edit/' + i.id" class="list-action" :title="$t('Edit')">
<i class="fa-solid fa-edit"></i>
</RouterLink>
<span @click="store.deleteItem(i.id)" class="list-action" :title="$t('Delete')">
<i class="fa-solid fa-trash"></i>
</span>
</div>
</div>
</div>
<div class="list-pagination">
<PaginateCustom
v-if="store.list.length != 0"
v-model="store.setPage"
:current_page="Number(store.current_page)"
:last_page="Number(store.last_page)"
/>
<NoRecords :show="store.list.length == 0" />
</div>
</template><!-- Runs only with vue 3.5 -->
<!-- <PaginateCustom :current_page="current_page" :last_page="last_page" @page="setPage" /> -->
<script setup>
import { onMounted, watchEffect } from 'vue';
const { current_page, last_page, offset } = defineProps({
current_page: { type: Number, default: 1 },
last_page: { type: Number, default: 10 },
offset: { type: Number, default: 3 },
btn_first: { type: String, default: 'First' },
btn_last: { type: String, default: 'Last' },
btn_next: { type: String, default: '>' },
btn_prev: { type: String, default: '<' },
});
const emits = defineEmits(['page']);
const model = defineModel();
let list = null;
let sublist = null;
let subpages = 2 * offset;
onMounted(() => {
freshList();
sliceList();
});
watchEffect(() => {
freshList();
sliceList();
});
function freshList() {
list = [...Array(last_page + 1).keys()];
list.shift();
}
function sliceList() {
sublist = list.slice(startIndex(), endIndex());
}
function startIndex() {
if (current_page > offset) {
// > offset
return current_page - offset - 1;
} else {
// < offset
return 0;
}
}
function endIndex() {
if (current_page <= offset) {
// <= offset
return subpages;
} else {
// > offset
return current_page + offset;
}
}
function scrollTop() {
window.scrollTo(0, 1);
}
function setPage(page) {
emits('page', page);
model.value = page;
scrollTop();
}
function prevPage() {
let page = current_page - 1;
page = page < 1 ? 1 : page;
setPage(page);
}
function nextPage() {
let page = current_page + 1;
page = page < last_page ? page : last_page;
setPage(page);
}
</script>
<template>
<div class="panel_list_links">
<div class="panel_paginate_link" @click="prevPage" v-if="current_page > 1">{{ $t(btn_prev) }}</div>
<div class="panel_paginate_link" @click="setPage(1)" v-if="current_page > 1">{{ $t(btn_first) }}</div>
<div class="panel_paginate_link" :class="{ panel_paginate_link_active: current_page == i }" v-for="i in sublist" @click="setPage(i)">{{ i }}</div>
<div class="panel_paginate_link" @click="setPage(last_page)" v-if="current_page < last_page">{{ $t(btn_last) }}</div>
<div class="panel_paginate_link" @click="nextPage" v-if="current_page < last_page">{{ $t(btn_next) }}</div>
</div>
</template>
<style>
.panel_list_links {
float: left;
width: 100%;
margin-block: 20px;
box-sizing: border-box;
}
.panel_paginate_link {
float: left;
font-size: 14px;
min-width: 40px;
padding: 9px 15px;
margin-top: 10px;
margin-right: 10px;
color: f1f1f1;
border: 1px solid #f1f1f1;
border-radius: var(--border-radius);
text-align: center;
cursor: pointer;
user-select: none;
}
.panel_paginate_link:hover {
color: var(--text-1);
background: var(--bg-3);
}
.panel_paginate_link_active {
color: #fff;
background: var(--accent);
border: 1px solid var(--accent);
font-weight: 600;
}
</style>