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

vue.js tutorial configurazione laravel

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>
 
// ...
laravel vue lista con categoria, relazione uno a molti
laravel vue lista con categoria, relazione uno a molti

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.

Laravel Vue campo filtro
Laravel Vue Campo Filtro

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.

Vue primo filtro
laravel vue primo filtro
Vue secondo filtro
laravel vue secondo filtro

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',
                                    }">&uarr;</span>
                                    <span :class="{
                                        'text-blue-600': orderDirection === 'desc' && orderColumn === 'id',
                                        'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'id',
                                    }">&darr;</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',
                                    }">&uarr;</span>
                                    <span :class="{
                                        'text-blue-600': orderDirection === 'desc' && orderColumn === 'title',
                                        'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'title',
                                    }">&darr;</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',
                                }">&uarr;</span>
                                    <span :class="{
                                    'text-blue-600': orderDirection === 'desc' && orderColumn === 'created_at',
                                    'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'created_at',
                                }">&darr;</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:

Elenco con filtro e ordinamento
Elenco con filtro e ordinamento

L’esempio fin qui descritto in questo articolo è disponibile su Github.

Letture Correlate

Ercole Palmeri

Autore