Build a realtime search feature with Adonis.js, Vue.js and Pusher
Knowledge of Javascript Knowledge of Node.js and NPM or Yarn Knowledge of Vue.js and Vuex
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:
# if you don't have Adonis CLI installed on your machine.
npm install -g @adonisjs/cli
# Create a new adonis app and move into the app directory
$ adonis new adonis-vue-pusher && cd adonis-vue-pusher
Now start the server and test if everything is working fine:
adonis serve --dev
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:
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:
#if you want to use npm
npm install pusher
#or if you prefer Yarn
yarn add pusher
Now that the SDK is installed, you can install the Pusher provider for Adonis with this command:
#if you want to use npm
npm install adonis-pusher
#or if you prefer Yarn
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
`start/app.js`:
const providers = [
...
'adonis-pusher/providers/Pusher'
]
Last, let’s install other dependencies that we’ll use to build our app.
Run this command in your terminal:
#if you want to use npm
npm install vue vuex axios laravel-mix pusher-js lodash mysql cross-env
#or if you prefer Yarn
yarn add vue vuex axios laravel-mix pusher-js lodash mysql cross-env
Dependencies we will use:
vue
andvuex
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 serverlodash
utility functions to manipulate our data on the frontendmysql
, Node.js driver for MySQL to set up our database as this app will use MySQL for storagecross-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:
"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",
"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",
"asset-watch-poll": "npm run watch -- --watch-poll",
"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",
"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:
const mix = require('laravel-mix');
mix.setPublicPath('public');
/*
|--------------------------------------------------------------------------
| 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 your application, as well as bundling up your JS files.
|
*/
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:
window._ = require('lodash');
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
window.axios.defaults.headers.common.crossDomain = true;
window.axios.defaults.baseURL = '/';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://adonisjs.com/docs/4.1/csrf');
}
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:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=your_database_user
DB_PASSWORD=your_dtabase_password
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:
//../database/migrations/*_product_schema.js
'use strict'
const Schema = use('Schema')
class ProductSchema extends Schema {
up() {
this.create('products', (table) => {
table.increments()
table.string('name')
table.integer('price')
table.string('image')
table.string('description')
table.timestamps()
})
}
down() {
this.drop('products')
}
}
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:
'use strict'
const Route = use('Route')
Route.get('/', 'ProductController.index')
Route.group(() => {
Route.get('/products', 'ProductController.get')
Route.get('/search', 'ProductController.search')
}).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:
'use strict'
const Event = use('Event')
const Product = use('App/Models/Product')
class ProductController {
async index({view}) {
return view.render('search')
}
async get({response}) {
let products = await Product.all()
return response.json(products)
}
async search({params, request, response}) {
// console.log(request.input('query'))
let query = request.input('query')
let products = await Product.query().where('name', 'like', '%' + query + '%')
.orWhere('description', 'like', '%' + query + '%').fetch()
Event.fire('search::results', products.toJSON())
return response.json('ok')
}
}
module.exports = ProductController
First lines import Event
service provider and Product
model
You can notice 3 functions in the code above:
index
renders thesearch.edge
file in theresources/views
directory (which is where views are stored in Adonis).get
fetches products from our database and returns them in a JSON formatsearch
gets the query sent in the request and returns every product whose name or description contains it, and returnsok
as the response. We also fire an event namedsearch::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:
//.env
PUSHER_APP_KEY=<APP_KEY>
PUSHER_APP_SECRET=<APP_SECRET>
PUSHER_APP_ID=<APP_ID>
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.
//events.js
const Pusher = use('Pusher')
const Event = use('Event');
const Env = use('Env');
// set up Pusher
let pusher = new Pusher({
appId: Env.get('PUSHER_APP_ID'),
key: Env.get('PUSHER_APP_KEY'),
secret: Env.get('PUSHER_APP_SECRET'),
cluster: Env.get('PUSHER_APP_CLUSTER'),
encrypted: false
});
//listening to events and send data with Pusher channels
Event.when('search::results', async (products) => {
pusher.trigger('search-channel', 'results', {
products
})
});
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:
//../resources/js/components/Products.vue
<template>
<div class="container">
<div class="row" v-if="products.length> 0" v-for="products in groupedProducts">
<div class="col-md-3 col-sm-6" v-for="product in products">
<product class="animated fadeIn" :product="product"></product>
</div>
<div class="col w-100"></div>
</div>
<div v-else >
<p class="text-center">No items</p>
</div>
</div>
</template>
<!--<script src=""></script>-->
<script>
import {mapGetters} from 'vuex'
import product from '../components/Product'
// const Pusher = require('pusher')
export default {
name: "Products",
components: {
product
},
mounted() {
this.$store.dispatch('GET_PRODUCTS')
let pusher = new Pusher('aac7493e27ef97acdfc6', {
cluster: 'eu',
encrypted: false
});
//Subscribe to the channel we specified in our Adonis Application
let channel = pusher.subscribe('search-channel')
channel.bind('results', (data) => {
this.$store.commit('SET_PRODUCTS', data.products)
})
},
computed: {
groupedProducts() {
return _.chunk(this.products, 4);
},
...mapGetters([
'products'
])
}
}
</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:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Realtime search with Adonis and Pusher</title>
<meta name="csrf-token" content="{{csrfToken}}">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css"/>
<!--{{ style('style') }}-->
</head>
<body>
<div id="app">
<div class="container">
<h5 class="text-center" style="margin-top: 32px">Realtime search feature with Adonis and Pusher</h5>
<br><br>
<searchbar></searchbar>
<products></products>
</div>
</div>
{{ script('js/app.js') }}
</body>
</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.
29 January 2019
by Ethiel Adiassa