Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active November 6, 2025 09:29
Show Gist options
  • Select an option

  • Save atomjoy/95361276829d14d1ca0dcef6e55e67a5 to your computer and use it in GitHub Desktop.

Select an option

Save atomjoy/95361276829d14d1ca0dcef6e55e67a5 to your computer and use it in GitHub Desktop.
Laravel pagination resource helper.

Pagination Resource Helper (Vue, Laravel)

Laravel pagination resource helper for vue custom pagination component with store.

Make all resource files

php artisan make:model Gif -arR

PaginarionResource.php

<?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(),
		];
	}
}

Model Resource

<?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);
	}
}

Resource controller

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));
	}
}

Vue store

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,
	};
});

Vue Paginate

<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>

PaginateCustom component

<!-- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment