In this tutorial, we will improve the posts table by adding the sorting feature.
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); }}
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>
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.
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.