How to Create a React Live Search Component with Images and Custom HTML in Laravel


I think everybody is familiar with the concept of Ajax Live Search ie a Search box that gives the suggestions that are updated as per the string entered by the user. These suggestions come in real-time from the connected database. Every day we are seeing it when we are using Google or YouTube where we get related results in the drop-down menu associated with input we type in the search field. Basically, everybody uses 3rd party javascript libraries to create Ajax Live Search. The Jquery UI Autocomplete and Twitter Typeahead are the most popular one. But one problem I usually face with these libraries is they are extremely difficult to customize according to your needs. You need to follow their rules and limitations. That is why I created a very simple custom Ajax Live Search with custom HTML and Images by using only Vue.js and Vanilla Javascript with almost all same functionality that Jquery UI Autocomplete and Twitter Typeahead offers.

In this tutorial, I will show you how to make a simple React Live search Component with Images and Custom HTML and implement it in your Laravel projects. This component is very simple to maintain and can even use outside of your Laravel project. I will be using Laravel as backend in this tutorial, But you can use anything as the only thing you really require is well-formatted JSON search result. Besides, React I will also use Axios for making HTTP API requests and Lodash for easily adding extra functionality in javascript. These are already included in Laravel if you are using Laravel mix starting from Laravel 5.4 and above.

If you are completely new to React or ES6 terminologies then I really suggest you to learn the basics before continuing this tutorial as React is a little more complicated than jQuery or Vue.js. If you love the simplicity and doesn't care about complex frameworks like React then I suggest you to check out my simplest version this tutorial using Vue.js by visiting the following link, 

 Ajax Live search with Images and Custom HTML in Laravel using Vue.js 

Let's Start Coding

First thing is to include React in our application. In the case of Laravel, you can easily include React using Laravel Mix.

1) Setup Laravel Mix

Laravel Mix provides a fluent API for defining Webpack build steps for your Laravel application using several common CSS and JavaScript pre-processors. All you need to do is install Nodejs and NPM in your development system and run

npm install

to install all javascript dependencies in your application. By default jQuery, Bootstrap, Vuejs, Axios and Lodash are already included in this setup.  Now just run

npm run watch 

to compile your assets. Don't forget to include app.css and app.js files in the public folders to your Laravel View Layouts.

Now there is a little twist. If you are not using Vue.js in your laravel projects then you can replace the Vue.js with React by just running the following commands.

php artisan preset react 

This command will swap out the Vue scaffolding with React scaffolding. The default Mix configuration, components, and all other related files will be updated accordingly. Don't forget to run npm install and npm run watch after executing this command. Please note that this will work only in Laravel 5.5 and above.

Now if you want to include React to an existing Laravel project (5.4 and above) without removing Vue.js then you can follow below instructions to include React without disrupting your old Vue components.

Replace your webpack.mix.js file with following snippets.

let mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.js('resources/assets/js/app.js', 'public/js')
   .react('resources/assets/js/react.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css');

 

As you can see that I am bundling my React components into a separate output file ie to public/js/react.js file. Don't forgot to include this file in your blades where you use React components.

After modifying webpack.mix.js file install the latest version or React and ReactDOM as your development dependency by running the following command.

npm install react react-dom --save-dev

Then run,

npm run watch 

This command will install babel-preset-react for the first time and now everything is done and you successfully included  React in your Laravel project.

2) Configure your routes file

Add the following route to your web.php file

Route::get('demos/reactsearch','DemoController@showReactSearch');
Route::post('demos/reactsearch','DemoController@getReactSearch');

 

The first route is for showing the Initial View Page and the second route is used by ajax call for fetching JSON search result from the database.

3) Setup your Controller 

Now Let's create our DemoController by running the following command.

php artisan make:controller DemoController

The complete code for DemoController is given below.

<?php

namespace App\Http\Controllers;

use Helper;
use App\Post;
use Illuminate\Http\Request;

class DemoController extends Controller
{
    /**
     * To display the show page
     *
     * @return \Illuminate\Http\Response
     */
    public function showReactSearch() 
    {
        return view('demos.reactsearch');
    }

    /**
     * To fetch the results from database
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function getReactSearch(Request $request)
    {
        $search =  $request->search;

        $posts = '';

        if (trim($request->search)) {
            $posts = Post::where('title','LIKE',"%{$search}%")
                         ->orderBy('created_at','DESC')->limit(5)->get();

            $posts = $posts->map(function ($post, $key) {
                            return [
                                        'title' => $post['title'],
                                        'url'  => url('blog/'.$post['slug']),
                                        'image' => Helper::catch_first_image($post['body']),
                                   ];
                        });          
        }

        return $posts;
    }


}

 

The showReactSearch method is used to just show our view page and getReactSearch does the actual database querying and fetching of the result. Although you can directly return $posts after the database querying I am not doing that because in order to fetch post images I need to call an external Helper function. So I am using map method in Laravel to run over the original collection and then to create a new associative array with only fields I needed. After that, I just return that collection as Laravel will automatically convert it to JSON when returning it. If you are wondering about catch_first_image custom helper function and how to create it you can refer the following Link.

Note: Both controller and routes parts will exclusively depend on your projects you are working on. They are unnecessary in this tutorial as only this they do is give some response data. I just added them only for reference purpose. 

4) Setup your Laravel View

Now let's create demo/reactsearch.blade.php file to include our React Component. The complete code of this file is given below.

@extends('main')

@section('title','React Live search Component with Images and Custom HTML in Laravel - ')

@section('content')

<div> 
    
    <p>&nbsp;</p>
    <h3 class="text-center title-color">
    React Live search Component with Images and Custom HTML in Laravel
    </h3>
    <p>&nbsp;</p>

    <!-- search box container starts  -->
    <div id="react-search"></div>
    <!-- search box container ends  -->

    
    <div style="padding-top:280px;" >
        <hr>
        <h5>For the complete tutorial of how to make this demo app visit the following 
            <a href="https://shareurcodes.com/blog/create-a-react-live-search-component-with-images-and-custom-html-in-laravel">Link</a>.
        </h5>
    </div>


</div>
    
@stop

@section('scripts')

<script type="text/javascript" src="{{ asset('js/react.js') }}"></script>  
    
@stop

 

Don't forget to include your bundled js file at bottom of your view file. Remember you will see some fancy class names but don't worry about it as CSS and HTML parts changes according to your requirements. I just used Material design lite and Bootstrap 3 in creating ShareurCodes.

4) Create React Components

Before you create your react components, don't forget to include them in your resourses/assets/js/app.js or in the resourses/assets/js/react.js file (If you are including React alongside with Vue and want to bundle it separately) as below.

// React Components
require('./components/ReactSearch');

 

Now Let's create our primary React Component. The complete code for the component/ReactSearch.js file is given below.

import React from 'react';
import ReactDOM from 'react-dom';
import List from './List';
import '../../sass/components/react-search.scss';

export default class ReactSearch extends React.Component {
  constructor(props) {
    super(props);

    this.state =  {
      posts : [],
      search : '',
      count : 0,
      width: 0,
    };

    this.handleSearch = this.handleSearch.bind(this);
    
    // Attaching Lodash debounce to avoid frequent ajax requests
    this.getPosts = _.debounce(this.getPosts, 500);
  }

  componentDidMount() {
    // get width of search input for react search widget on initial load
    const width = document.getElementById("search").offsetWidth; 
    this.setState(() => ({ width : width }));

    // get width of search input for react search widget when page resize
    window.addEventListener('resize', (e) => {
      const newWidth = document.getElementById('search').offsetWidth; 
      this.setState(() => ({ width : newWidth }));
    });

    // To clear react search widget when click on body
    document.body.addEventListener('click', (e) => {
      this.clearData(e);
    });

    document.getElementById('search').addEventListener('keydown', (e) => {
      // check whether up or down arrow keys are pressed
      if (e.keyCode === 38 || e.keyCode === 40) {
        // To prevent cursor from moving left or right in text input
        // You can only prevent it in keydown event
        // If you only use keyup then event already fired
        e.preventDefault();
      }
    });
  }

  handleSearch(e) {
    // check whether arrow keys are pressed using Loadash
    if(_.includes([37, 38, 39, 40, 13], e.keyCode) ) {
      if (e.keyCode === 38 || e.keyCode === 40) {
        // To prevent cursor from moving left or right in text input
        e.preventDefault();
      }

      if (e.keyCode === 40 && this.state.posts == "") {
        // If post list is cleared and search input is not empty 
        // then call ajax again on down arrow key press 
        this.getPosts();
        return;
      }
         
      this.selectPost(e.keyCode);
    } else {
      this.getPosts();
    }
  }

  getPosts() {
    this.setState(() => ({ 
      posts: [],
      count: 0,
      search: this.refs.newSearch.value      
    }));

    if (this.state.search.trim() != '') {
      axios.post("/demos/reactsearch",{
          search : this.state.search
      })
     .then( (response) => {
        this.setState(() => ({ posts : response.data }));
      })
     .catch( (error) => {
          console.log(error);
      });  
    }
  }

  selectPost(keyCode) {
    // If down arrow key is pressed
    if (keyCode == 40 && this.state.count < this.state.posts.length) {
      this.setState((prevState) => ({ count : prevState.count + 1 }));
    }
    // If up arrow key is pressed
    if (keyCode == 38 && this.state.count > 1) {
      this.setState((prevState) => ({ count : prevState.count - 1 }));
    }
    // If enter key is pressed
    if (keyCode == 13) {
      // Go to selected post
      document.getElementById(this.state.count).childNodes[0].click();
    }
  }

  clearData(e) {
    if (e.target.id != 'search') {
      this.setState(() => ({ 
        posts: [],
        count: 0
      }));
    }
  }

  render() {
    const ulStyle = {
      width : this.state.width + 'px'
    }

    const posts = this.state.posts.map((post, index) => (
      <List
        key = {index}
        post = {post}
        index = {index + 1}
        count = {this.state.count}
      />
    ));

    return (
      <div className="well">
        <div className="row">
          <div className="col-sm-2">
            <span className="input-group-addon span-style" >
              BLOG SEARCH
            </span>
          </div>

          <div className="col-sm-10">
            <input type="text" autoComplete="off" 
              onKeyUp={this.handleSearch}
              id="search" ref="newSearch"  
              className="form-control input-lg" 
              placeholder="Enter Blog Title Here" 
            />
            
            {this.state.posts.length > 0 && 
              <ul style={ulStyle} className="widget" >
                { posts }
              </ul>
            }

          </div>
        </div>
      </div>
    );

  }
}

if (document.getElementById('react-search')) {
  ReactDOM.render(<ReactSearch />, document.getElementById('react-search'));
}

 

As you can see I am registering 3 javascript Event listeners when the component is mounted. The resize Event Listener is registered to make our app listen to every browser window resize event so that we can recalculate the search widget's width thus make our application more responsive. The click Event Listener is registered to make our component listen to every HTML body's click event so that we can hide our search popup widget when someone clicks on outside the search input field.

Note: I also registered keydown Event Listener even though I am also listening to keyup Event because I need to prevent cursor movement when I am pressing up and down arrow keys inside the search box. If I only used Keyup Event then I cannot prevent it because the event already triggered. But I also need to use Keyup Event since Keydown event is one character behind when updating the search state.

Note: If you are using this component outside Laravel projects don't forget to globally include both Axios and Lodash.

The keyUp Event will trigger handleSearch method when the input field changes. Inside this Method, I am checking whether it is a arrows keys and enter key press or not because we will be calling separate methods when they pressed. _.includes is a Lodash exclusive helper method which helps us to easily check whether a required value is in the array.

I am using _.debounce helper method of Lodash to delay the getPosts method call by 500 milliseconds thus prevent unwanted frequent ajax call to the server on every key press. The getPosts method is used to send ajax post request to the server and to fetch its result. I am using Axios for doing that. Since the result is already in JSON format, The React will automatically parse it to Javascript Object Array. 

Now Let's create our nested Functional Stateless React Component. The complete code for the component/List.js file is given below.

import React from 'react';

const List = (props) => (
  <li id={props.index} 
    className={ (props.index === props.count) ? 'active menu-item' : 'menu-item'} 
  >
    <a href={props.post.url} >
      <div className="list_item_container" title={props.post.title}>
        <div className="image">
            <img src={props.post.image} />
        </div>
        <div className="label">
            <h4>{ props.post.title  }</h4>
        </div>
      </div>
    </a>
  </li>
);

export default List;

 

We will be passing out individual post as a prop to this component. This component just displays post title and image inside a li tags. Beside post, we also pass count and index as props to this component. This is used to activate active class based on our selection. The selectPost method increment or decrement count state by one when we press up or down arrow in the search box. It also triggers click event to link associated with each List component.

Now before winding up Let's add some styling to our demo application. The complete code for sass/components/react-search.scss file is given below.

.active {
    background-color: #eaeaea;
}

/* Styling Vue Search Widget - You can customize it as you wish */
.widget {
    border: 1px solid #c5c5c5;
    background: white;
    position: absolute;
    cursor: pointer;
    list-style: none;
    padding: 0;
    z-index: 100;
}
.menu-item{
    height: 80px;
    border: 1px solid #ececf9;
}
.list_item_container {
    width: 98%;
    float: left;
}

.image {
    width: 10%;
    float: left;
    padding: 10px;
}
.image img{
    width: 80px;
    height : 60px;
}
.label{
    width: 89%;
    float:right;
    color: rgb(124,77,255);
    font-weight: bold;
    text-align: left;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}
@media only screen and (max-width:768px) {

    .image img{
        width: 70px;
        height : 55px;
    }
    .label{
        width: 70%;
    }
    .label h4{
        font-size: 15px;
    }
}

/* To get a indigo focus color in search input - Not nessary */
input[type="text"]:focus{
  border-color: #5b518b !important; 
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px #5b518b !important;
  outline: 0 none;
}

/* Custom style Blog Search Span Box - Not necessary */
.span-style {
    color: white;
    background-color: #5b518b;
    height: 46px;
}

 

Feel free to customize it according to your taste and application. I just customize it according to my demo app look and feel. One important thing is your widget position should be absolute so that it floats over other elements. 

The output image of above program is given below.

 React Live search Component with Images and Custom HTML in Laravel

The Demo

You can demo the above application by visiting following link.

https://shareurcodes.com/demos/reactsearch

If anybody found this method difficult and time taking then feel free to use jQuery UI Autocomplete and Twitter Typeahead. I already made a simple tutorial about jQuery UI Autocomplete with Images and Custom HTML in Laravel and Twitter Typeahead integration in Laravel for you guys and feel free to check them by visiting following links.

jQuery UI Autocomplete with Images and Custom HTML in Laravel

Twitter Typeahead integration in Laravel

If anybody has any suggestions or doubts or need any help comment below and I try will respond to every one of you as early as possible.


Web development
12th May 2018 10:05:35 PM
PHP Laravel Javascript Ajax React
13296

ShareurCodes

ShareurCodes is a code sharing site for programmers to learn, share their knowledge with younger generation.