Laravel 11 e Vue.js: pagina Vue con campo filtro e ordinamento

Laravel 11 e Vue.js è una combinazione ottimale per scrivere codice pulito ed efficiente.
In questo tutorial vediamo insieme come creare una pagina con Vue.js in laravel 11 con filtro e ordinamento.
Per provare Vue.js creeremo un componente vue, e una relazione uno a molti, su cui implementare funzionalità come filtro e ordinamento.
Tempo di lettura stimato: 5 minuti
L’esempio descritto in questo articolo è disponibile su Github.
Per leggere e comprendere al pieno questo articolo si consiglia la lettura dell’articolo Laravel 11 e Vue.js: installazione, configurazione ed esempi, e implementazione del codice che puoi trovare su repository GitHUB.
Relazione Uno a Molti
Come prima operazione creiamo un modello Category
, e una migration
.
php artisan make:model Category -m
Il file xxxx_create_categories_table.php diventa:
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
Il modello Category.php diventa:
class Category extends Model
{
protected $fillable = [
'name',
];
}
Successivamente, dobbiamo creare una migrazione per aggiungere il campo category_id
per la relazione verso la tabella Posts
.
php artisan make:migration "add category to posts table"
La migrazione database/migrations/xxxx_add_category_to_posts_table.php è da completare come segue:
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->foreignId('category_id')->after('content')->constrained();
});
}
Nel modello abbiamo anche bisogno di una relazione.
App/Modelli/Post.php:
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
protected $fillable = [
'title',
'content',
'category_id',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
Nel controller PostController
dobbiamo caricare le categorie.
App/Http/Controllers/Api/PostController.php:
class PostController extends Controller
{
public function index()
{
$posts = Post::with('category')->paginate(10);
return PostResource::collection($posts);
}
}
Avendo impostato il PostResource
per definire il Json, dobbiamo aggiungere l’informazione Category
.
App/Http/Risorse/PostResource.php:
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => substr($this->content, 0, 50) . '...',
'category' => $this->category->name,
'created_at' => $this->created_at->toDateString()
];
}
}
Tutto ciò che rimane da fare, è mostrare la categoria nel frontend. Modifichiamo l’Index.vue
aggiungendo la categoria vicino al titolo.
Risorse/js/components/Posts/Index.vue:
<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
// ...
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Category</span>
</th>
// ...
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
<tr v-for="post in posts.data">
// ...
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.title }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.category }}
</td>
// ...
</tr>
</tbody>
</table>
<TailwindPagination :data="posts" @pagination-change-page="getPosts" class="mt-4" />
</div>
</div>
</template>
// ...

Filtro con selezione DropDown
Andiamo ora a creare una selezione a discesa, per scegliere da un elenco di categorie. Per fare questo, andiamo a creare un nuovo Composable
e un nuovo endpoint API
con l’API Resource
.
Creiamo un Controller con il percorso API.
php artisan make:controller Api/CategoryController
php artisan make:resource CategoryResource
app/Http/Controllers/Api/CategoryController.php:
use App\Http\Resources\CategoryResource;
class CategoryController extends Controller
{
public function index()
{
return CategoryResource::collection(Category::all());
}
}
routes/api.php:
use App\Http\Controllers\Api\CategoryController;
Route::get('posts', [PostController::class, 'index']);
Route::get('categories', [CategoryController::class, 'index']);
Nella risorsa aggiungeremo solo i campi id
e name
.
app/Http/Resources/CategoryResource.php:
class CategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
];
}
}
Ora che abbiamo un endpoint API
, possiamo creare un nuovo Composable
e utilizzarlo per ottenere le categorie. Il composable
per le categorie sarà quasi identico a quello che abbiamo per i post.
resources/js/composables/categories.js:
import { ref } from 'vue'
export default function useCategories() {
const categories = ref({})
const getCategories = async () => {
axios.get('/api/categories')
.then(response => {
categories.value = response.data.data;
})
}
return { categories, getCategories }
}
Successivamente, dobbiamo aggiungere le categorie nel Composable componente Vue PostsIndex
e mostrare tutte le categorie nell’input selezionato sopra la tabella.
resources/js/components/Posts/Index.vue:
<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
<div class="mb-4">
<select v-model="selectedCategory" class="block mt-1 w-full sm:w-1/4 rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="" selected>-- Filter by category --</option>
<option v-for="category in categories" :value="category.id" :key="category.id">
{{ category.name }}
</option>
</select>
</div>
// ...
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { TailwindPagination } from 'laravel-vue-pagination';
import usePosts from "@/composables/posts";
import useCategories from "@/composables/categories";
const selectedCategory = ref('')
const { posts, getPosts } = usePosts()
const { categories, getCategories } = useCategories()
onMounted(() => {
getPosts()
getCategories()
})
</script>
Dopo aver visitato la pagina si dovrebbe vedere l’elenco di selezione.

Attivazione Filtro con selezione DropDown
Per ottenere tutti i post che appartengono a una categoria, dobbiamo gestire la variabile selectedCategory
. In primo luogo, dobbiamo importare watch
dal Vue.
Risorse/js/components/Posts/Index.vue :
<template>
// ...
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { TailwindPagination } from 'laravel-vue-pagination';
import usePosts from "@/composables/posts";
import useCategories from "@/composables/categories";
// ...
</script>
Ora usando questa funzione watch()
, possiamo gestire la variabile selectedCategory
per le modifiche con due valori: current
e previous
.
All’interno di watch
possiamo chiamare il getPosts
che già accetta la pagina, ma ora possiamo anche passare il valore current
della categoria selezionata.
Risorse/js/components/Posts/Index.vue :
<script setup>
import { onMounted, ref, watch } from "vue";
// ...
watch(selectedCategory, (current, previous) => {
getPosts(1, current)
})
</script>
Successivamente, dobbiamo modificare i post Composable
in modo che getPosts
accetterà la categoria, che per impostazione predefinita sarà una stringa vuota.
E lo stesso per il parametro della pagina, passare la categoria alla richiesta API.
Risorse/js/composables/posts.js :
import { ref } from 'vue'
export default function usePosts() {
const posts = ref({})
const getPosts = async (page = 1, category = '') => {
axios.get('/api/posts?page=' + page + '&category=' + category)
.then(response => {
posts.value = response.data;
})
}
return { posts, getPosts }
}
E ora, tornando al back-end: dobbiamo aggiungere una clausola condizionale alla query Eloquent
in cui otteniamo i post.
App/Http/Controllers/Api/PostController.php :
use Illuminate\Database\Eloquent\Builder;
class PostController extends Controller
{
public function index()
{
$posts = Post::with('category')
->when(request('category'), function (Builder $query) {
$query->where('category_id', request('category'));
})
->paginate(10);
return PostResource::collection($posts);
}
}
Dobbiamo passare la pagina come parametro.
Risorse/js/components/Posts/Index.vue :
php artisan make:controller Api/CategoryController
<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
// ...
<TailwindPagination :data="posts" @pagination-change-page="page => getPosts(page, selectedCategory)" class="mt-4" />
</div>
</div>
</template>
<script setup>
// ...
</script>
Ora la pagina con il filtro funziona come previsto.


Ordinare i dati facendo clic sulle intestazioni della colonna
Aggiungiamo la funzione di ordinamento, partiamo dal Back-end.
app/Http/Controllers/Api/PostController.php:
class PostController extends Controller
{
public function index()
{
$orderColumn = request('order_column', 'created_at');
$orderDirection = request('order_direction', 'desc');
$posts = Post::with('category')
->when(request('category'), function (Builder $query) {
$query->where('category_id', request('category'));
})
->orderBy($orderColumn, $orderDirection)
->paginate(10);
return PostResource::collection($posts);
}
}
Il controller
riceve order_column
e order_direction
dall’URL. I valori predefiniti sono “created_at
” e “desc
“.
Successivamente aggiungiamo la validazione dei parametri di ordinamento, per motivi di sicurezza, per verificare se tali parametri hanno valori accettabili.
app/Http/Controllers/Api/PostController.php:
class PostController extends Controller
{
public function index()
{
$orderColumn = request('order_column', 'created_at');
if (! in_array($orderColumn, ['id', 'title', 'created_at'])) {
$orderColumn = 'created_at';
}
$orderDirection = request('order_direction', 'desc');
if (! in_array($orderDirection, ['asc', 'desc'])) {
$orderDirection = 'desc';
}
$posts = Post::with('category')
->when(request('category'), function (Builder $query) {
$query->where('category_id', request('category'));
})
->orderBy($orderColumn, $orderDirection)
->paginate(10);
return PostResource::collection($posts);
}
}
Ora, in modo simile come abbiamo fatto con la categoria, dobbiamo aggiungere parametri alla funzione getPosts
nel post Composable
.
resources/js/composables/posts.js
import { ref } from 'vue'
export default function usePosts() {
const posts = ref({})
const getPosts = async (
page = 1,
category = '',
order_column = 'created_at',
order_direction = 'desc'
) => {
axios.get('/api/posts?page=' + page +
'&category=' + category +
'&order_column=' + order_column +
'&order_direction=' + order_direction)
.then(response => {
posts.value = response.data;
})
}
return { posts, getPosts }
}
Nel Componente Vue PostsIndex
, abbiamo bisogno di aggiungere le due variabili: chiamiamole orderColumn
e orderDirection
.
resources/js/components/Posts/Index.vue:
<script setup>
import { onMounted, ref, watch } from "vue";
import { TailwindPagination } from 'laravel-vue-pagination';
import usePosts from "@/composables/posts";
import useCategories from "@/composables/categories";
const selectedCategory = ref('')
const orderColumn = ref('created_at')
const orderDirection = ref('desc')
const { posts, getPosts } = usePosts()
const { categories, getCategories } = useCategories()
onMounted(() => {
getPosts()
getCategories()
})
watch(selectedCategory, (current, previous) => {
getPosts(1, current)
})
</script>
Ora dobbiamo aggiungere le frecce alle intestazioni delle colonne della tabella, per mostrare la possibilità di ordinare i dati, lo faremo in corrispondenza delle colonne ID
, Title
e Created at
.
resources/js/components/Posts/Index.vue:
<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
// ...
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer" @click="updateOrdering('id')">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider" :class="{ 'font-bold text-blue-600': orderColumn === 'id' }">
ID
</div>
<div class="select-none">
<span :class="{
'text-blue-600': orderDirection === 'asc' && orderColumn === 'id',
'hidden': orderDirection !== '' && orderDirection !== 'asc' && orderColumn === 'id',
}">↑</span>
<span :class="{
'text-blue-600': orderDirection === 'desc' && orderColumn === 'id',
'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'id',
}">↓</span>
</div>
</div>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer" @click="updateOrdering('title')">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider" :class="{ 'font-bold text-blue-600': orderColumn === 'title' }">
Title
</div>
<div class="select-none">
<span :class="{
'text-blue-600': orderDirection === 'asc' && orderColumn === 'title',
'hidden': orderDirection !== '' && orderDirection !== 'asc' && orderColumn === 'title',
}">↑</span>
<span :class="{
'text-blue-600': orderDirection === 'desc' && orderColumn === 'title',
'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'title',
}">↓</span>
</div>
</div>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Category</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Content</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer" @click="updateOrdering('created_at')">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider" :class="{ 'font-bold text-blue-600': orderColumn === 'created_at' }">
Created at
</div>
<div class="select-none">
<span :class="{
'text-blue-600': orderDirection === 'asc' && orderColumn === 'created_at',
'hidden': orderDirection !== '' && orderDirection !== 'asc' && orderColumn === 'created_at',
}">↑</span>
<span :class="{
'text-blue-600': orderDirection === 'desc' && orderColumn === 'created_at',
'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'created_at',
}">↓</span>
</div>
</div>
</th>
</tr>
</thead>
// ...
</table>
<TailwindPagination :data="posts" @pagination-change-page="page => getPosts(page, selectedCategory)" class="mt-4" />
</div>
</div>
</template>
<script setup>
// ...
</script>
Se orderColumn
è uguale a quello che si sta ordinando, cambiamo il testo in colore blu in grassetto, usando :class
. Lo stesso vale per le frecce. Controlliamo la direzione e la colonna e in base a ciò mostriamo o nascondiamo la freccia.
Infine procediamo a implementare l’ordinamento mediante il metodo updateOrdering
che definiremo in Index.vue
(script) e richiamato da Index.vue
(template) su evento click su frecce @click="updateOrdering('created_at')"
.
<template>
// ...
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { TailwindPagination } from 'laravel-vue-pagination';
import usePosts from "@/composables/posts";
import useCategories from "@/composables/categories";
const selectedCategory = ref('')
const orderColumn = ref('created_at')
const orderDirection = ref('desc')
const { posts, getPosts } = usePosts()
const { categories, getCategories } = useCategories()
const updateOrdering = (column) => {
orderColumn.value = column
orderDirection.value = (orderDirection.value === 'asc') ? 'desc' : 'asc'
getPosts(1, selectedCategory.value, orderColumn.value, orderDirection.value)
}
// ...
</script>
Il metodo inverte l’ordinamento e richiama la getPosts per rileggere i record nel nuovo ordinamento. Il risultato è il seguente:

L’esempio fin qui descritto in questo articolo è disponibile su Github.
Letture Correlate
- Laravel 11 e Vue.js: installazione, configurazione ed esempi
- Laravel 11 e Vue.js: progettazione pagina Vue, Datatable con Laravel API
- Come utilizzare Laravel con Vue.js 3
- Creazione di una App CRUD con Laravel e Vue.js
- Vue e Laravel: creare una Single Page Appliction
Ercole Palmeri