Courses

Vue.js 3 + Laravel 11 API + Vite: SPA CRUD

Sorting Data by Clicking Column Headings

Summary of this lesson:
- Adding column-based sorting functionality
- Implementing sorting indicators in table headers
- Managing sort state in Vue component
- Handling sort parameters in Laravel API

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

Previous: Category Dropdown: Filtering Data
avatar

In this article was missed how to fix pagination. If you have 100 items, each page contains 10 items, and will order by id desc list will starts from 100, but if you go to 2nd page you will miss counting and it will show you list start from 20 to 30.

avatar

Use the Limit property https://laravel-vue-pagination.org/guide/api/props.html#limit

avatar

it's not very convenient to manually create query parameters by concatenation, axios has params options as second params. details: https://axios-http.com/docs/req_config

avatar
You can use Markdown
avatar

In case anyone missed it.

//Add orderColumn and orderDirection in watcher

watch(selectedCategory, (current, previous) => { getPosts(1, current, orderColumn.value, orderDirection.value) })

//Add orderColumn and orderDirection in TailwindPagination too

<TailwindPagination :data="posts" @pagination-change-page="page => getPosts(page, selectedCategory, orderColumn, orderDirection)"

👍 2
😍 1
avatar
You can use Markdown
avatar

How to add order by category.name in the post table?

avatar
You can use Markdown
avatar
You can use Markdown