Skip to content

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:

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

  1. 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.

  2. Now we will need to create a route that will handle the search request. Head to routes/api.php and add the following route:

    php
    Route::get('/search/{query}', [SearchController::class, 'search']);

    This route will handle the search request and will call the search method from the SearchController we created earlier.

  3. 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

html
<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

html
<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.

  1. 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 the onMounted 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 the watchDebounced 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.

  2. 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 the click event to change the activeTab reference to users. 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).

All Rights Reserved