Laravel Passport REST API + Vuero search page
This is a tutorial, made by Denis Neagoe, for setting up a REST API endpoint for your Vuero project and in particular for creating a search page.
INFO
If you think your app can work with cookies instead of authentication by Bearer token
, We recommend you that you to use Laravel Breeze instead. This tutorial will only focus on how the implementation could be made using Laravel Passport, but you can also use Laravel Breeze (remember to add the CSRF-Token
request).
Before getting started you might want to check the documentation of Laravel Passport as it might not serve your purpose. We recommend using Laravel Passport if you're going to allow your users to use OAuth2.
Everything in this page can be found on the Laravel official docs and on the VueUse docs:
- https://laravel.com/docs/9.x/installation
- https://laravel.com/docs/9.x/passport
- https://vueuse.org/shared/watchDebounced/
We have already covered the installation of Laravel Passport in the Laravel Passport + Vuero tutorial, so we will only focus on this searching implementation.
Laravel Controller & Route
In order to create a search page, we will need to create a controller that will handle the search request. Run
php artisan make:controller SearchController
and edit the file to:php<?php namespace App\Http\Controllers; use App\Models\Article; use Illuminate\Http\Request; class SearchController extends Controller { /** * Display the specified resource. * * @param string $query * @return \Illuminate\Http\JsonResponse */ public function search($query) : JsonResponse { $users = User::where('username', 'ilike', "%$query%") ->orWhere('email', 'ilike', "%$query%") ->get(); return response()->json($users); } }
This controller will handle the search request and return the results as a JSON response. For the purpose of this tutorial, we will only search for users, but you can easily extend this to search for other models. Also, we are using the
ilike
operator to make the search case insensitive so that it allows you to find records easier.Now we will need to create a route that will handle the search request. Head to
routes/api.php
and add the following route:phpRoute::get('/search/{query}', [SearchController::class, 'search']);
This route will handle the search request and will call the
search
method from theSearchController
we created earlier.At this point all the required additions have been made to your Laravel application. In order for your new route to work, you will need to run
php artisan optimize
to re-create the compiled file which will include the new route.
Components' changes
You might want to change the behavior of the PlaceloadV1
and EmptySearch
components to match your needs. In my case I have decided to keep the categories sidebar and only change the content of the right column so you can use the following code: PlaceloadV1.vue
<template>
<div class="list-view list-view-v1">
<div class="list-view-inner">
<!--Item-->
<div v-for="key in 10" :key="key" class="list-view-item">
<VPlaceloadWrap>
<VPlaceloadAvatar size="medium" />
<VPlaceloadText last-line-width="60%" class="mx-2" />
<VPlaceload class="mx-2" disabled />
<VPlaceload class="mx-2 h-hidden-tablet-p" />
<VPlaceload class="mx-2 h-hidden-tablet-p" />
<VPlaceload class="mx-2" />
</VPlaceloadWrap>
</div>
</div>
</div>
</template>
You can keep the <style>
tag as it is needed.
EmptySearch.vue
<template>
<!--Search Placeholder -->
<div class="page-placeholder">
<div class="placeholder-content">
<img class="light-image" src="/@src/assets/illustrations/placeholders/search-7.svg" alt="" />
<img class="dark-image" src="/@src/assets/illustrations/placeholders/search-7-dark.svg" alt="" />
<h3>We couldn't find any matching results.</h3>
<p class="is-larger">Too bad. Looks like we couldn't find any matching results for the search terms you've entered. Please try different search terms or criteria.</p>
</div>
</div>
</template>
In EmptySearch
we don't need the <style>
tag so you can remove it.
Vuero search page
Your Vuero project should already have a page called search-results which we will need. We will be also using the PlaceloadV1
and EmptySearch
components.
Head over to your
search-results
page and then make the following changes:ts<script setup lang="ts"> import { useLaravelFetch } from '/@src/composable/useLaravelFetch' import { useNotyf } from '/@src/composable/useNotyf' import { formatError } from '/@src/composable/useError' import { type User } from '/@src/models/user' const $fetch = useLaravelFetch() const notyf = useNotyf() const totalRecords = ref<number>(0) const users = ref<Partial<User[]>>([]) const isLoading = ref(false) const search = ref('') const loadUsers = async () => { try { isLoading.value = true const data = await $fetch('/panel/users') users.value = data totalRecords.value = data.length } catch (err: any) { notyf.error(formatError(err)) } finally { isLoading.value = false } } watchDebounced( search, async () => { if (search.value) try { isLoading.value = true const data = await $fetch('/panel/users/search/' + search.value) users.value = data totalRecords.value = data.length } catch (err: any) { notyf.error(formatError(err)) } finally { isLoading.value = false } else loadUsers() }, { debounce: 250, maxWait: 1000 } ) type TabId = 'all' | 'users' | 'records' const activeTab = ref<TabId>('all') const options = ref(['Newest']) onMounted(() => { loadUsers() }) </script>
For the sake of the tutorial, we will be leaving the previous records array just to fill the gap.
The code above will allow you to make an API request to the Laravel application and fetch the users. We will also be using the
useNotyf
composable to display error messages if something goes wrong. After making the request, your page will get populated with the records fetched either by theonMounted
listener, or by the API call made to the search route. To also limit the number of requests made to the server, we will be using thewatchDebounced
function to make the request only after the user stops typing for 250ms and this will also prevent the server from being overloaded with requests.For displaying the new records, you should make the following changes:
html<VField raw> <VControl icon="feather:search"> <VInput v-model="search" placeholder="Search again..." /> </VControl> </VField> <div class="search-info"> <span v-if="isLoading"> Loading records</span> <span v-else> {{ totalRecords }} record{{ totalRecords > 1 ? 's' : '' }} found </span> </div>
This will allow us to bind the search input's value to the
search
reference and will also add a placeholder for the counter. Under<div class="tabs-inner">
we will edit the line to match the following:html<div class="tabs" :class="[totalRecords < 1 && 'is-disabled']"></div>
This will allow us to disable the tabs while searching for a result.
html<li :class="[activeTab === 'users' && 'is-active']"> <a tabindex="0" @keydown.space.prevent="activeTab = 'users'" @click="activeTab = 'users'"><span>Users</span></a> </li>
This will change the tab's name to
Users
and will also add a listener to theclick
event to change theactiveTab
reference tousers
. In order to display the users, head over to<!--Results-->
and change the lines to match the following:html<PlaceloadV1 v-if="isLoading" /> <div v-else-if="totalRecords > 0" class="column is-8"> <div class="tab-content" :class="[activeTab === 'all' && 'is-active']"> <!--Search Results Group--> <div class="search-results-group"> <div class="group-header"> <VIconWrap icon="feather:user" /> <h4>Users</h4> </div> <div class="search-results-list"> <!--Item--> <div v-for="user in users" :key="user!.id" class="search-results-item"> <VBlock :title="user!.username" :subtitle="user!.email" center> <template #icon> <VAvatar size="medium" :picture="user!.profile_photo_path" /> </template> <template #action> <VButton dark-outlined> Profile </VButton> </template> </VBlock> </div> </div> </div> <!--Search Results Group--> <div class="search-results-group"> <div class="group-header"> <VIconWrap icon="feather:file-text" /> <h4>Records</h4> </div> <div class="search-results-list"> <!--Item--> <div v-for="(record, index) in records" :key="index" class="search-results-item" > <VBlock :title="record.name" :subtitle="record.date" center> <template #icon> <VAvatar size="medium" :picture="record.preview" :squared="record.squared" /> </template> <template #action> <VButton dark-outlined> Details </VButton> </template> </VBlock> </div> </div> </div> </div> <div class="tab-content" :class="[activeTab === 'users' && 'is-active']"> <!--Search Results Group--> <div class="search-results-group"> <div class="search-results-list"> <div v-for="user in users" :key="user!.id" class="search-results-item"> <VBlock :title="user!.username" :subtitle="user!.email" center> <template #icon> <VAvatar size="medium" :picture="user!.profile_photo_path" /> </template> <template #action> <VButton dark-outlined> Profile </VButton> </template> </VBlock> </div> </div> </div> </div> <div class="tab-content" :class="[activeTab === 'records' && 'is-active']"> <!--Search Results Group--> <div class="search-results-group"> <div class="search-results-list"> <!--Item--> <div v-for="(record, index) in records" :key="index" class="search-results-item" > <VBlock :title="record.name" :subtitle="record.date" center> <template #icon> <VAvatar size="medium" :picture="record.preview" :squared="record.squared" /> </template> <template #action> <VButton dark-outlined> Details </VButton> </template> </VBlock> </div> </div> </div> </div> </div> <EmptySearch v-else-if="!isLoading && totalRecords < 1" /> </PlaceloadV1
INFO
Everything is done and you should be able to fetch your users by making these changes. However, you can extend the use of the page by searching into different models and displaying the results in the same page (instead of records for example).