Laravel Jetstream Search Input

Adam Bailey • December 28, 2021

laravel vue

Share:

I recently needed to incorporate search functionality on a Laravel Jetstream application using Inertia. This post outlines how I did that.

What is Laravel Jetstream?

Laravel Jetstream:

Laravel Jetstream is a beautifully designed application starter kit for Laravel and provides the perfect starting point for your next Laravel application. Jetstream provides the implementation for your application's login, registration, email verification, two-factor authentication, session management, API via Laravel Sanctum, and optional team management features.

I really love using Laravel to develop my applications, and for me, this was a no-brainer. Jetstream comes packed full of features I won't have to implement or worry about. These features already come with tests, so I can ensure im not breaking them as I go. I can build whatever I want inside it and use the pre-implemented components as I go. I am able to move pretty fast inside this architecture.

I chose to use the Inertia stack. I really love using Vue.js so this was also a very easy decision for me. When you choose this stack, it also comes with a whole range of Vue components that you can reuse throughout building your app.

However, one component that it did not come with was a search input for searching models.

I'm going to assume you already have a working instance of Laravel Jetstream working locally, and we'll start from there.

The Basic Controller

Let's say you want to fetch a list of stories from the database. You have a route like this:

1Route::get('/stories', [StoryController::class, 'index'])->name('stories.index');

and a controller method iin StoryController called index which looks like this, to get your stories and render them via Inertia to a page in Vue:

1public function index(Request $request): Response
2{
3 return Inertia::render('Stories', [
4 'stories' => Story::paginate(),
5 ]);
6}

The Basic Vue Page

Then on the Vue side, you receive the stories from the backend via a stories prop to this basic template:

1<template>
2 <app-layout>
3 <template #header> Stories </template>
4 
5 <div class="flex justify-between items-center">
6 <div class="text-3xl">Stories</div>
7 </div>
8 
9 <div v-if="stories.data.length">
10 <ul>
11 <li v-for="(story, index) in stories.data" :key="index">
12 <span>{{ story.title }}</span>
13 </li>
14 </ul>
15 </div>
16 
17 <div v-else class="text-gray-500">No stories were found.</div>
18 </app-layout>
19</template>
20 
21<script>
22 import { defineComponent } from "vue";
23 import AppLayout from "@/Layouts/AppLayout.vue";
24 
25 export default defineComponent({
26 components: {
27 AppLayout,
28 },
29 
30 props: {
31 stories: Object,
32 },
33 });
34</script>

Nothing fancy here. It's just going to return 15 stories if you have them, and this doesn't even give you a way to paginate to the next page. But its enough to work with for the purposes of this blog post.

New Search Input Component

We're going to make a new Vue component, just put it in a Components directory.

Here's the entire component:

1<template>
2 <div class="w-1/2 bg-white px-4">
3 <label for="search" class="hidden">Search</label>
4 <input
5 id="search"
6 ref="search"
7 v-model="search"
8 class="transition h-10 w-full bg-gray-100 border border-gray-500 rounded-full focus:border-purple-400 outline-none cursor-pointer text-gray-700 px-4 pb-0 pt-px"
9 :class="{ 'transition-border': search }"
10 autocomplete="off"
11 name="search"
12 placeholder="Search"
13 type="search"
14 @keyup.esc="search = null"
15 @blur="search = null"
16 />
17 </div>
18</template>
19 
20<script>
21 import { defineComponent } from "vue";
22 
23 export default defineComponent({
24 props: {
25 // any route name from laravel routes (ideally index route is what you'd search through)
26 routeName: String,
27 },
28 
29 data() {
30 return {
31 // page.props.search will come from the backend after search has returned.
32 search: this.$inertia.page.props.search || null,
33 };
34 },
35 
36 watch: {
37 search() {
38 if (this.search) {
39 // if you type something in the search input
40 this.searchMethod();
41 } else {
42 // else just give us the plain ol' paginated list - route('stories.index')
43 this.$inertia.get(route(this.routeName));
44 }
45 },
46 },
47 
48 methods: {
49 searchMethod: _.debounce(function () {
50 this.$inertia.get(
51 route(this.routeName),
52 { search: this.search },
53 { preserveState: true }
54 );
55 }, 500),
56 },
57 });
58</script>

We have a:

Incorporate Vue Search Component

Use the new component in the header of your Stories page like so:

1<div class="flex justify-between items-center">
2 <div class="font-header text-3xl md:text-5xl">Stories</div>
+ <search-input route-name="stories.index" />
4</div>

And into the script area:

1<script>
2 import { defineComponent } from "vue";
3 import AppLayout from "@/Layouts/AppLayout.vue";
4 // add below line to import component
+ import SearchInput from "@/Components/SearchInput";
6 
7 export default defineComponent({
8 components: {
9 // add below line to register component to template
+ SearchInput,
11 AppLayout,
12 },
13 
14 props: {
15 stories: Object,
16 },
17 });
18</script>

Set Up Backend to Accept and Return Search

Since we don't yet have anything in our controller to receive the search value, nor do we have the inertia prop available to us in vue, we need to make some changes to the StoriesController.index method.

Here is the updated controller with everything we need:

1public function index(Request $request): Response
2{
3 // get our search value from the request here:
4 $search = $request->search;
5 
6 $stories = Story::query()
7 // when we have a search value, see if it matches anything in the title or content of the story:
8 ->when($search,
9 fn ($query) => $query->where('title', 'LIKE', '%'.$search.'%')
10 ->orWhere('content', 'LIKE', '%'.$search.'%')
11 )
12 ->paginate();
13 
14 return Inertia::render('Stories', [
15 'stories' => $stories,
16 // and return the search value as a page prop to inertia/vue.
17 // This is the value we watch in the data() property of the SearchInput.vue component.
18 'search' => $search,
19 ]);
20}

Conclusion

At this point you should have a working search input in your page which automatically starts searching your model as you type into the input. Hopefully you are able to understand this concept enough to incorporate it into your application.

You should be able to reuse this component for any pages in your app that return an index route. You just simply change the routeName prop, then set up the backend logic to process the search and return its value:

1Route::get('/whatevers', [WhateverController::class, 'index'])->name('whatevers.index');
1// in WhateverController.index
2$search = $request->search;
3 
4// query search LIKE %whatever blah blah%
5$whatevers = Whatever::query()->when($search, fn ($query) => $query->where('something', 'LIKE', '%'.$search.'%'));
6 
7// return it all
8return Inertia::render('Whatevers', [
9 'whatevers' => $whatevers,
10 'search' => $search,
11]);
1<search-input route-name="whatevers.index" />

I have certainly been enjoying how fast I can code out my ideas using Laravel Jetstream as a starting point for an app.

Hopefully this adds some value to your experience!

You may also find my article outlining how to apply the SpeechRecognition API to this component useful. Please see VueJs Search Input With SpeechRecognition API.

Happy coding!

You might like other posts in
laravel