Back to Course |
Vue.js 3 + Laravel 11 + Vite: SPA CRUD

Sorting Data by Clicking Column Headings

In this tutorial, we will improve the posts table by adding the sorting feature.

ordering data


Back-end: Sorting in Laravel

We will start from the 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);
}
}

We will pass the order_column and order_direction as parameters from the URL. The default values will be "created_at" and "desc".

Now we need to add validation for security reasons, to check if those parameters have valid values.

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

Composable Parameters and Vue Variables

Now, similarly as we did with the category, we need to add parameters to the post Composable getPosts function.

resources/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)
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 }
}

In the PostsIndex Vue component, we need to add two variables: let's call them orderColumn and 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>

Visual Table: Arrows and Colors

Now we need to add arrows to the table column headings, to show the directions.

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">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">ID</span>
<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">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
<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">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Created at</span>
<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>

If the orderColumn is equal to the one that is ordering then we change the text to bold and blue color, using :class binding.

The same goes for the arrows. We check the direction and column and according to that we either show or hide the arrow.

default sort


Vue Method: Update Ordering

We added a new action above: @click="updateOrdering('created_at')

The new method updateOrdering accepts the column. We create it in the component below.

resources/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";
 
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>

In this function, we set the orderColumn to the one we clicked.

For the orderDirection, it needs to be the opposite to the current value. So if it's ascending then we need to set it to descending, and vice versa.

And lastly, we need to call the getPosts by passing all the parameters.

By default, the table is ordered by a created_at field desc. If you click on any other column, now it will be ordered by that column.

ordered by title