In this tutorial, we will improve the posts table by adding the sorting feature.
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', }">↑</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"> <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', }">↑</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"> <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', }">↑</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>
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.
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.
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.
Use the Limit property https://laravel-vue-pagination.org/guide/api/props.html#limit
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
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)"
How to add order by category.name in the post table?