Build a realtime search feature with Adonis.js, Vue.js and Pusher

Introduction

Build a realtime search feature with Adonis.js, Vue.js and Pusher

Introduction

In this tutorial, we will build a realtime search feature with Vue.js and Pusher that you can integrate in your app using Adonis.js, as the backend framework. We’ll explore some of Adonis’ cool features and add a realtime taste to our app by using Pusher.

Adonis is an open source MVC Node.js framework inspired by the Laravel PHP framework, so if you have ever used Laravel you’ll feel at ease with Adonis.js. Adonis takes features we love working with in Laravel and combines it with the speed and efficiency of the Node ecosystem.

Prerequisites

In order to follow this tutorial, knowledge of Javascript and Node.js is required. You should also have the following installed on your machine:

Set up the Adonis project

First open your terminal and type this command to install Adonis CLI and create a new adonis app:

1# if you don't have Adonis CLI installed on your machine. 
2      npm install -g @adonisjs/cli
3      
4    # Create a new adonis app and move into the app directory
5    $ adonis new adonis-vue-pusher && cd adonis-vue-pusher

Now start the server and test if everything is working fine:

1adonis serve --dev
2    
3    2018-09-23T12:25:30.326Z - info: serving app on http://127.0.0.1:3333

Open your browser and make a request to : http://127.0.0.1:3333. You should see the following:

image_missing

Install the Pusher SDK and other dependencies

We won’t use the Pusher SDK directly but instead use a Pusher provider for Adonis. But we should first install the Pusher SDK by running this command:

1#if you want to use npm
2    npm install pusher
3    
4    #or if you prefer Yarn
5    yarn add pusher

Now that the SDK is installed, you can install the Pusher provider for Adonis with this command:

1#if you want to use npm
2    npm install adonis-pusher
3    
4    #or if you prefer Yarn
5    yarn add adonis-pusher

This provider helps us easily use the Pusher SDK with the Adonis.js framework. You will need to add the provider to AdonisJS at

1`start/app.js`:
2
3
4    const providers = [
5        ...
6        'adonis-pusher/providers/Pusher'
7    ]

Last, let’s install other dependencies that we’ll use to build our app. Run this command in your terminal:

1#if you want to use npm
2    npm install vue vuex axios laravel-mix pusher-js lodash mysql cross-env
3    
4    #or if you prefer Yarn
5    yarn add vue vuex axios laravel-mix pusher-js lodash mysql cross-env

Dependencies we will use:

  • vue and vuex to build the frontend of our app,
  • axios to make HTTP requests to our API endpoints
  • laravel-mix to provide a clean, fluent API for defining basic webpack build steps
  • pusher-js to listen to events emitted from our server
  • lodash utility functions to manipulate our data on the frontend
  • mysql, Node.js driver for MySQL to set up our database as this app will use MySQL for storage
  • cross-env to run scripts that set and use environment variables across platforms

Set up our build workflow

We’ll use laravel-mix to build and compile our application assets in a fluent way. But first we must tell our app to use it for that purpose. Open your package.json file and paste the following in the scripts section:

1"asset-dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
2    "asset-watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
3    "asset-watch-poll": "npm run watch -- --watch-poll",
4    "asset-hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
5    "asset-prod": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"

After that create a webpack.mix.js file at the root of your project and paste this code:

1const mix = require('laravel-mix');
2   
3   mix.setPublicPath('public');
4   /*
5    |--------------------------------------------------------------------------
6    | Mix Asset Management
7    |--------------------------------------------------------------------------
8    |
9    | Mix provides a clean, fluent API for defining some Webpack build steps
10    | for your Laravel application. By default, we are compiling the Sass
11    | file for your application, as well as bundling up your JS files.
12    |
13    */
14    
15   mix.js('resources/assets/js/app.js', 'public/js')

The code above builds, compiles and bundles all our javascript code into a single js file created automatically in public/js directory.

Now create this file assets/js/bootstrap.js and paste this code inside:

1window._ = require('lodash');
2    
3    window.axios = require('axios');
4    
5    window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
6    window.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
7    window.axios.defaults.headers.common.crossDomain = true;
8    window.axios.defaults.baseURL = '/';
9    
10    let token = document.head.querySelector('meta[name="csrf-token"]');
11    
12    if (token) {
13      window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
14    } else {
15      console.error('CSRF token not found: https://adonisjs.com/docs/4.1/csrf');
16    }
17    
18    window.Pusher = require('pusher-js');

You will notice we require dependencies to build our app. We also globally registered some headers to the axios library in order to handle some security issues and to tackle in a proper way our API endpoints. These headers enable respectively ajax request, define Content-Type for our post requests, CORS and register the CSRF token.

Next, create this file: assets/js/app.js and paste the following inside:

    require('./bootstrap')

When we import our bootstrap.js file , laravel-mix will compile our app.js file. Our app is now ready to use laravel-mix for building and compiling our assets. By running this command: npm run asset-dev you should see a public/js/app.js file after the build process. Great!

Set up the database and create the migration

We’ll use a MySQL database for storage in this tutorial. Open your .env file and update the database section with your own identifiers:

1DB_CONNECTION=mysql
2    DB_HOST=127.0.0.1
3    DB_PORT=3306
4    DB_USER=your_database_user
5    DB_PASSWORD=your_dtabase_password
6    DB_DATABASE=your_database_name

Open your terminal and run this command to generate our Product model as well as its corresponding controller and migration file which will be used to build the schema of our products table:

    adonis make:model Product -mc

Inside your product migration file copy and paste this code:

1//../database/migrations/*_product_schema.js
2    'use strict'
3    
4    const Schema = use('Schema')
5    
6    class ProductSchema extends Schema {
7      up() {
8        this.create('products', (table) => {
9          table.increments()
10          table.string('name')
11          table.integer('price')
12          table.string('image')
13          table.string('description')
14          table.timestamps()
15        })
16      }
17    
18      down() {
19        this.drop('products')
20      }
21    }
22    
23    module.exports = ProductSchema

This code is pretty similar to what we are accustomed to in Laravel migration. You can see we defined our products table fields as:

  • name
  • price
  • image
  • description

The increments() will create an id field with Auto Increment and set it as Primary key. The timestamps() will create the created_at and updated_at fields respectively.

Now if your run this command: adonis migration:run in your terminal it will create a products table in your database. I recommend you to import this schema in your database to create and populate your products table with dummy data.

Define routes and create the controller

In this part we’ll create our routes and define controller functions responsible for handling our HTTP requests. We are going to create 3 basic routes for our application, one for rendering our app view, one for search query and another one for fetching products from our database. Go to the start/routes.js file and replace the content with:

1'use strict'
2    
3    const Route = use('Route')
4    
5    Route.get('/', 'ProductController.index')
6    
7    Route.group(() => {
8      Route.get('/products', 'ProductController.get')
9      Route.get('/search', 'ProductController.search')
10    }).prefix('api')

This block pulls in Route service provider. Routes defining in Adonis is similar to the Laravel methodology and you should not have any problems if you have worked with Laravel. We prefixed two of our routes with api to help remind us that they are api endpoints.

Next let’s create our controller functions. Open your ProductController.js file and paste the following:

1'use strict'
2    const Event = use('Event')
3    const Product = use('App/Models/Product')
4    
5    class ProductController {
6    
7      async index({view}) {
8        return view.render('search')
9      }
10    
11      async get({response}) {
12        let products = await Product.all()
13        return response.json(products)
14      }
15    
16      async search({params, request, response}) {
17    
18        // console.log(request.input('query'))
19        let query = request.input('query')
20    
21        let products = await Product.query().where('name', 'like', '%' + query + '%')
22          .orWhere('description', 'like', '%' + query + '%').fetch()
23    
24        Event.fire('search::results', products.toJSON())
25    
26        return response.json('ok')
27      }
28    
29    }
30    
31    module.exports = ProductController

First lines import Event service provider and Product model You can notice 3 functions in the code above:

  • index renders the search.edge file in the resources/views directory (which is where views are stored in Adonis).
  • get fetches products from our database and returns them in a JSON format
  • search gets the query sent in the request and returns every product whose name or description contains it, and returns ok as the response. We also fire an event named search::results with the query results in a JSON format. We can listen to this event and manipulate the data it carries.

Pusher setup

Pusher is a hosted cloud service that makes it super-easy to add realtime data and functionality to web and mobile applications. Pusher is an abstracted real-time layer between clients and servers. First, let's setup Pusher for our application. Head over to Pusher and create an account. You can sign in if you already have a account.

Next, create a new Pusher app instance. This registration provides credentials which can be used to communicate with the created Pusher instance. Copy the App ID, Key, Secret, and Cluster from the App Keys section and put them in the .env file located at you project root:

1//.env
2        PUSHER_APP_KEY=<APP_KEY>
3        PUSHER_APP_SECRET=<APP_SECRET>
4        PUSHER_APP_ID=<APP_ID>
5        PUSHER_APP_CLUSTER=<APP_CLUSTER>

Emit event with Pusher channels

Create a file name event.js in the start directory . In this file we’ll create an event which will be fired every time we need to send a message via Pusher channels, and in our use case we’ll send the search results via Pusher channels.

1//events.js
2    
3    const Pusher = use('Pusher')
4    const Event = use('Event');
5    const Env = use('Env');
6    
7    // set up Pusher
8    let pusher = new Pusher({
9      appId: Env.get('PUSHER_APP_ID'),
10      key: Env.get('PUSHER_APP_KEY'),
11      secret: Env.get('PUSHER_APP_SECRET'),
12      cluster: Env.get('PUSHER_APP_CLUSTER'),
13      encrypted: false
14    });
15    
16    //listening to events and send data with Pusher channels
17    Event.when('search::results', async (products) => {
18      pusher.trigger('search-channel', 'results', {
19        products
20      })
21    
22    });

We need to pull in the Event, Pusher (from adonis-pusher we installed earlier in the tutorial) and Env service providers. Next, we create a Pusher instance and configured with the credentials that were received after creating a Pusher account.

Next, we registered a listener for the search::results event, after which we initialize and configure Pusher. This event was registered in the ProductController.search function we created above to handle the search request.

When we are done with the pusher configuration, we trigger a results event on the search-channel with the trigger method.

Set up Vuex store

We’ll be using the Vuex library to centralize our data and control the way it is mutated throughout our application. To make things simple, I’ll redirect you to an earlier tutorial I wrote for how to setup Vuex.

Building Vue components

We’ll build three Vue components for our app, a Searchbar component, a Product component and a Products component, each of them responsible for a single functionality.

For the first two components Searchbar.vue and Product.vue I’ll also refer you to my tutorial on Laravel, Vue and Pusher as nothing changes about these components logic. We’ll be using the same code.

Create the Products.vue component This component will render products items from database. It’s that simple. So create your Products.vue component and paste this code inside:

1//../resources/js/components/Products.vue
2    
3    <template>
4      <div class="container">
5        <div class="row" v-if="products.length> 0" v-for="products in groupedProducts">
6          <div class="col-md-3 col-sm-6" v-for="product in products">
7            <product class="animated fadeIn" :product="product"></product>
8          </div>
9          <div class="col w-100"></div>
10        </div>
11        <div v-else >
12          <p class="text-center">No items</p>
13        </div>
14      </div>
15    </template>
16    
17    
18    <!--<script src=""></script>-->
19    <script>
20      import {mapGetters} from 'vuex'
21      import product from '../components/Product'
22    
23      // const Pusher = require('pusher')
24    
25      export default {
26        name: "Products",
27        components: {
28          product
29        },
30        mounted() {
31          this.$store.dispatch('GET_PRODUCTS')
32    
33          let pusher = new Pusher('aac7493e27ef97acdfc6', {
34            cluster: 'eu',
35            encrypted: false
36          });
37    
38          //Subscribe to the channel we specified in our Adonis Application
39          let channel = pusher.subscribe('search-channel')
40    
41          channel.bind('results', (data) => {
42            this.$store.commit('SET_PRODUCTS', data.products)
43          })
44    
45        },
46        computed: {
47          groupedProducts() {
48            return _.chunk(this.products, 4);
49          },
50          ...mapGetters([
51            'products'
52          ])
53        }
54      }
55    </script>

For a deeper explanation of this component, please refer to this section of my tutorial as nothing changes much except we initialize a Pusher instance using the credentials we obtained earlier when we were creating our Pusher app. Next we will subscribe to the search-channel and listen to the results event in order to commit the SET_PRODUCTS mutation with the data pulled in by the event.

Finalize the app

Now, let’s create our search.edge file which contains our three Vue.js components. Paste this code inside:

1<!DOCTYPE html>
2    <html lang="en">
3    <head>
4      <meta charset="UTF-8"/>
5      <title>Realtime search with Adonis and Pusher</title>
6      <meta name="csrf-token" content="{{csrfToken}}">
7    
8      <meta name="viewport"
9            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
10    
11      <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
12            integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
13    
14      <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
15    
16      <!-- Bootstrap core CSS -->
17      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
18            integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
19    
20      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"/>
21      <!--{{ style('style') }}-->
22    </head>
23    <body>
24    
25    <div id="app">
26    
27      <div class="container">
28        <h5 class="text-center" style="margin-top: 32px">Realtime search feature with Adonis and Pusher</h5>
29        <br><br>
30        <searchbar></searchbar>
31        <products></products>
32      </div>
33    
34    </div>
35    {{ script('js/app.js') }}
36    
37    </body>
38    </html>

We are almost done! Now open your terminal and run npm run asset-dev to build your app in a proper way. This can take a few seconds. After this step, run adonis serve --``dev if it wasnt’ done and open your browser to localhost:3333 to see if your app is working fine. Try searching for a product name or its description in the search bar, you should get realtime results for your search. You are now a boss 😎.

Conclusion

In this tutorial we’ve created a realtime search engine using Adonis, Vue.js, and Pusher to provide realtime functionality. Adonis is a great framework as it enhances developer productivity and allows us to quickly build apps. Combined with Pusher channels, you can build really cool realtime apps with Adonis.js. You can find the full source code for this tutorial on GitHub here and can think of new ideas to extend the application. It’ll be fun to see what you come up with! The source code for this tutorial is available on GitHub.