Thursday, March 28

Building a Google search clone SPA with Vue and Flask

Introduction

A few weeks ago, in the course of seeking for a developer role, I completed a technical assessment test and the entirety of this article is based on the development process; I thought it would be great to write about how I completed it, so open up your favourite code editor and let’s build something awesome and learn a few tricks.

The technical assessment test asked that I built an app with the following features:

Before anything else, I pulled out my pen and tore out a piece of paper, drew a basic representation of the app and decided to make it into a Google search clone. The app could have been anything really, but it sounds much better to tell my friends that I just built a Google search clone, don’t you think?

I decided to use Vue for the frontend and Flask for the backend, again, I could have used any other frameworks but these were the ones that came to my mind at that instant. Here is a demo of the final application:

The image above shows a Google search clone that searches through the saved content in a database and returns the article that best matches the search query. The drop-down underneath the search bar suggests article topics from the database as a search query is inserted.

The source code for this project is available here on GitHub.

Prerequisites

To follow along with this tutorial, a basic knowledge of Python, Flask, JavaScript and Vue is required. You will also need the following installed on your machine:

Virtualenv is a great tool for creating isolated Python environments, so we can install dependencies in an isolated environment, and not pollute our global packages directory.

Let’s install virtualenv with this command:

    $ pip install virtualenv

⚠️ Virtualenv comes pre-installed with Python 3 so you may not need to install it if you are already on this version.

This tutorial is split into two sections, the first section shows how to build the backend API using Flask while the other focuses on building the SPA and consuming the backend API with Vue.

Let’s get started.

Setting up the project

Let’s create the working directory and activate a new virtual environment:

    $ mkdir google-search-spa
    $ cd google-search-spa
    $ virtualenv .venv
    $ source .venv/bin/activate

Now that we have spun up a new virtual environment, we can install Flask within it:

    $ pip install flask

Building the backend logic

This is the first of the two sections that make up this article, here we write all the code that define the operations of the Flask backend server.

Creating the database handler file

In this application, we will save the entered article records to an SQLite database so we need to create a Python file that is able to create and interact with a database. In order to keep things modular and easy to debug, this file will only deal with database interactions and we will create another file to handle the API requests.

In the root directory of this project, create a new file and name it dbsetup.py, open the file and paste the following code:

# dbsetup.py

import sqlite3, json
from sqlite3 import Error

def create_connection(database):
    try:
        conn = sqlite3.connect(database, isolationlevel=None, checksamethread = False)
        conn.row_factory = lambda c, r: dict(zip(_[_col_[_0] for col in c.description], r))

        return conn
    except Error as e:
        print(e)

def create_table(c):
    sql = """ 
        CREATE TABLE IF NOT EXISTS articles (
            id integer PRIMARY KEY,
            name varchar(225) NOT NULL,
            body varchar(255) NOT NULL
        ); 
    """
    c.execute(sql)

def insert_db(c, name, body):
    sql = ''' INSERT INTO articles(name, body)
                VALUES (?, ?) '''
    c.execute(sql, (name, body))

def select_all_items(c, name):
    c.execute("SELECT * FROM articles WHERE name like ?", ('%'+name+'%',))
    rows = c.fetchall()
    return rows

def main():
    database = "./pythonsqlite.db"

    # create a database connection
    conn = create_connection(database)

    # create items table
    create_table(conn)

    # confirm that table is created
    print("Connection established!")

if _**name == '**_main_':
    main()

In the code above, we have defined the database handler functions that will perform the following operations:

Next, we will import these handler functions into a new Python file and use them in resolving our API endpoints.

Defining the endpoints and attaching the matching handler functions

Let’s create a new file and call it app.py. This file will be the main entry point to the backend server. Here, we will import the database handler functions. Open the app.py file and paste the following code:

# app.py

from flask import Flask, request, jsonify, make_response
from dbsetup import createconnection, selectallitems, insertdb, createtable
import json

app = Flask(__name)
database = "./pythonsqlite.db"

# create a database connection
conn = create_connection(database)

# create items table
create_table(conn)

c = conn.cursor()

def main():
    global c

@app.route('/search', methods=_[_'GET'])
def search():
    data = request.args.get("name")
    output = select_all_items(c, data)
    return json.dumps(output)

@app.route('/insert', methods=_[_'POST'])
def insert():
    content = request.json
    name = content_[_'name']
    body = content_[_'body']
    insert_db(c, name, body)
    return "Successful"

if _**name == '**_main_':
    main()
    app.run(debug=True)

In the code snippet above, we imported Flask and other dependencies including the database handler functions that we defined in the dbsetup.py file. Then we invoked two of the database handlers; one to create a new database connection and the other to create the ‘articles’ table.

Next, we defined two endpoints:

This is all the code that we need to write for the creation of the backend API. We will start building the frontend of this project in the next section.

Building the frontend and consuming the backend API

In this section, we will build the frontend of the application using the Vue cli tool, if you do not already have Vue cli installed on your machine, you can install it with this command:

    sudo npm install -g vue-cli

When the installation is completed, we will create a new Vue project with this command:

    vue init webpack spa

Vue cli helps us scaffold templates, and we have choosen the ‘webpack’ template here. During the creation process of this project, you will be presented with a number of query prompts. You can hit the enter key for each one of them (including the installation of the Vue router) except ‘ESLint’ and unit tests. We do not want to set up unit tests and ‘ESlint’ for this application so hit the ‘n’ key instead.

Let’s navigate into the Vue project directory and run the application with these commands:

    cd spa
    npm start

The application will be served on this address http://127.0.0.1:8080:

Before we start writing our code to render the Google search clone page, let’s create two Vue components in the components folder and name them:

Updating the index.html file

To help the view of the application look nice, we will pull in Bootstrap and Fontawesome from a CDN in the index.html file. Open the index.html file and update it with the following code:

// index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
    <title>THE Search SPA</title>
  </head>
  <body style="width: 100%; height: 100%">
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

The index.html file is the entry point to the frontend of this application so we only need to pull in these dependencies here and they will be available across the components.

Configuring the Vue router

Let’s configure the Vue router so that we will be able to switch between the pages in the application without causing a refresh/reload to the browser window. It is the possibility of this concept that describes the term — Single page application. Open the spa/src/main.js file and update it with the following code:

    // The steps are numbered below

    // 1
    import Vue from 'vue'

    // 2
    import App from './App'

    // 3
    import VueRouter from 'vue-router'

    // 4
    import Searchbox from './components/Searchbox'

    // 5
    import Insert from './components/Insert'

    // 6
    Vue.use(VueRouter)

    // 7
    const routes = [

    { path: '/', component: Searchbox },

    { path: '/insert', component: Insert }

    ]

    // 8
    const router = new VueRouter({
      routes, 
      mode: 'history'
    })

    // 9
    new Vue({

      el: '#app',

      template: '<App/>',

      components: { App },

      router

    }).$mount('#app')

Here’s a breakdown of what we’ve done in the code snippet above:

Updating the App component

Let’s update the root component and include the router outlet; this will ensure that whenever a link is clicked in the Vue application, a matching component is served without refreshing the browser window. Open the src/App.vue file and update it with the following code:

    // src/App.vue

    <template>
      <div id="app">

      <router-link v-bind:to="'/'">HOME</router-link>   |       <router-link v-bind:to="'/insert'">INSERT</router-link>
      <router-view></router-view>

      </div>
    </template>

    <script>

    export default {
      name: 'app',
    }

    </script>
    <style>

    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
      text-decoration: none;
    }

    </style>

We will save this file and run our application using this command:

    npm start

We will see that our application is currently a blank page with two links ‘HOME’ and ‘INSERT’:

The interesting fact about this blank page is that the browser window doesn’t refresh whenever any one of the two links is clicked. This means that the Vue router works and the application is currently an SPA. The interface is currently blank because we haven’t written any code in the Insert.vue and Searchbox.vue components. Let’s write some code in these components next.

Building the view for inserting articles into the database

Let’s write the code that will allow us to insert new article records into the database, open the src/components/Insert.vue file and update it with the following code:

    // src/components/Insert.vue

    <template>
    <div>
    <section>

    <div class="container">
    <form>
    <input type="name" ref="name" placeholder="Name" required>
    <textarea ref="body" placeholder="Body" required></textarea>
    <button name="send" type="submit" v-on:click.prevent="insertData" class="submit">SEND</button>

    </form>

    </div>
    </section>
    </div>
    </template>

    <script>

    // The JavaScript code will go here

    </script>

    <style scoped>

    form {
      margin: 6em 0em;
    }

    section .container form input,
    section .container form textarea {
      width:97.4%;
      height:30px;
      padding:5px 10px;
      font-size: 12px;
      color:#999;
      letter-spacing:1px;
      background: #FFF;
      border:2px solid #FFF;
      margin-bottom:25px;
      -webkit-transition:all .1s ease-in-out;
      -moz-transition:all .1s ease-in-out;
      -ms-transition:all .1s ease-in-out;
      -o-transition:all .1s ease-in-out;
      transition:all .1s ease-in-out;}
    section .container form input:focus,
    section .container form textarea:focus {
      border:2px solid #00b0ff;
      color:#999;
      }

    section .container form textarea {
      height:150px;
      }

    section .container form .submit {
      width:100%;
      padding:5px 10px;
      font-size: 12px;
      letter-spacing:1px;
      background:#0091ea;
      height:40px;
      text-transform:uppercase;
      letter-spacing:1px;
      color:#FFF;
      -webkit-transition:all .1s ease-in-out;
      -moz-transition:all .1s ease-in-out;
      -ms-transition:all .1s ease-in-out;
      -o-transition:all .1s ease-in-out;
      transition:all .1s ease-in-out;
      }

    section .container form .submit:hover {
      color:#FFF;
      background: #00b0ff;
      cursor:pointer;
      }

    section .container form .required {
      color:#b43838;
      }

    </style>

In the script section of the code snippet above, we will use the JavaScript Fetch API to POST inserted articles to the backend server. Let’s include the code that does this between the script tags:

    // src/components/Insert.vue

    export default {
      name: 'insert',
      methods: {
        insertData: function(){
          let name = this.$refs.name.value
          let  body = this.$refs.body.value
          fetch('/api/insert', {
            method: 'POST',
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json'
        },
        body: JSON.stringify({name: name, body: body})
      });
      this.$refs.name.value = ""
      this.$refs.body.value = ""
        }
      }
    }

Building the view for retrieving articles from the database

Open the src/components/Searchbox.vue file and update it with the following code.

    // src/components/Searchbox.vue

    <template>
    <div>
    <nav class="navbar navbar-default navbar-fixed-top navbar-transparent">

          <div class="container-fluid">
              <ul class="nav navbar-nav navbar-right nav-large-font">
                <li><a href="#">Gmail</a></li>
                <li><a href="#">Images</a></li>
                <li><a href="#"><span class="glyphicon glyphicon-th"></span></a></li>
                <li><a href="#"><button class="btn btn-sign-in">Sign in</button></a></li>
              </ul>
          </div>

        </nav>

        <div class="container">
          <div class="container-center">
            <div class="row">
              <div class="text-center col-md-12">

                <img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="Google Logo" id="google-logo">

                <div class="form-adjustment">
                  <form action="#" class="form-inline">
                    <div class="form-group">
                      <div class="input-group">
                        <input type="text" class="form-control input-lg" ref="searchbox" list="huge_list" v-on:keyup="hinter" id="gg-search">
                        <div class="input-group-addon input-group-addon-transparent"><i class="fa fa-microphone fa-lg"></i></div>
                      </div>                        <datalist id="huge_list" ref="huge_list"></datalist>
                    </div>
                  </form>
                </div>

              </div>

              <div class="list-results-container container vertical-center row">
              <div class="list-results col-md-12 text-center"  v-for="(response, index) in responseData">
              <h4 style="color: #609;"> {{ response.name }}  </h4>
              <p style="color: #545454;"> {{ response.body }}  </p>
              <hr/>
              </div>
              </div>

            </div>
          </div>
        </div>

        <footer class="navbar navbar-default navbar-fixed-bottom">

          <div class="container-fluid">

              <ul class="nav navbar-nav">                 <li><a href="#">Advestising</a></li>
                <li><a href="#">Business</a></li>
                <li><a href="#">About</a></li>
              </ul>
              <ul class="nav navbar-nav navbar-right">
                <li><a href="#">Privacy</a></li>
                <li><a href="#">Terms</a></li>
                <li><a href="#">Settings</a></li>
                <li><a href="#">Thank you :)</a></li>
              </ul>

          </div>

        </footer>
        </div>
    </template>

    <script>

    // The JavaScript code will go here

    </script>

    <style scoped>
    .list-results-container {
    margin: 2em 0 8em 0;
    }
    .list-results {
      padding: 0.5em;
    }
    .navbar-default .nav-large-font > li > a {
      color: #000;
      font-size: 1.0625em;
    }
    .navbar-transparent {
      background-color: transparent;
      border-bottom: none;
    }
    button.btn-sign-in {
      font-weight: bold;
      color: #fff;
      background-color: #4683ea;
    }
    .navbar-nav>li, .navbar-nav {
    float: left !important;
    }
    .navbar-nav.navbar-right:last-child {
    margin-right: -15px !important;
    }
    .navbar-right {
    float: right !important;
    }
    #google-logo {
      width: 272px;
      height: 92px;
    }
    /_ FORM _/
    .form-adjustment {
      margin-top: 5em;
    }
    #gg-search {
      width: 500px;
    }
    .input-group-addon-transparent {
      background-color: transparent;
    }
    /_ BUTTONS _/
    .button-adjustment {
      margin-top: 50px;
    }
    .button-adjustment button {
      background-color: #f2f2f2;
      color: #757575;
      font-family: arial, sans-serif;
      font-size: 13px;
      font-weight: bold;
    }

    @media (max-width: 300px) {
        .navbar.navbar-default.navbar-fixed-top {
            display: none;
        }
    }
    </style>

In the script section of the code snippet above, we will write some code to invoke a hinter() function whenever there is a ‘key-up’ event on the search bar element. Here’s a list of things the hinter() function will do when invoked:

Let’s include the JavaScript code between the script tags:

    // src/components/Searchbox.vue

    export default {

      name: 'Searchbox',
      data () {
        return {
            responseData: []
        }
      },

      mounted() {
        // create one global XHR object 
        // so we can abort old requests when a new one is make
        window.hinterXHR = new XMLHttpRequest();
      },

      methods: {
        hinter: function(event) {

        // retain access to the Vue instance
        var app = this

        // retireve the input element
        var input = event.target;

        // retrieve the datalist element
        var huge_list = this.$refs.huge_list

        // display new set of results for each query
        var results = this.$refs.results

        // minimum number of characters before we start to generate suggestions
        var min_characters = 3;

        if (input.value.length < min_characters ) { 

          app.responseData = [];
          return;

        } else { 

            // abort any pending requests
            window.hinterXHR.abort();

            window.hinterXHR.onreadystatechange = function() {

                if (this.readyState == 4 && this.status == 200) {

                    // We're expecting a json response so we convert it to an object
                    var response = JSON.parse( this.responseText ); 

                    // clear any previously loaded options in the datalist and responseData array
                    huge_list.innerHTML = ""
                    app.responseData = response

                    response.forEach(function(item) {
                        // Create a new <option> element.
                        var option = document.createElement('option');
                        option.value = item.name;
                        // attach the option to the datalist element
                        huge_list.appendChild(option);
                    });
                }
            };

            window.hinterXHR.open("GET", "/api/search?name=" + input.value, true);
            window.hinterXHR.send()
        }
    }
      }
    }

We can start the Vue dev server now and see what we have built so far:

npm start

Looks great! However our Vue frontend is not in sync with the Flask backend yet. We need to create an API proxy table so that the frontend and backend can communicate with eachother.

Creating an API proxy table

To communicate with the backend server from the Vue application, we need to create an API proxy table in the config/index.js file and start the Vue dev server and the backend server side-by-side. All requests to /api in our Vue frontend code will be proxied to the backend server.

Open the config/index.js and make the following modifications:

        // config/index.js

        module.exports = {
          // ...
          dev: {
            // ...
            proxyTable: {
                '/api': {
                target: 'http://localhost:5000',
                changeOrigin: true,
                pathRewrite: {
                  '^/api': ''
                }
              }
            },
            // ...
          }
        }

In the proxyTable we attempt to proxy any requests to the /api address to localhost:5000.

Testing the application

We can test the application by starting the Vue dev server and the Flask backend server side-by-side. We can achieve this by having two terminals pointed to the appropraite directories, the first terminal should be pointed to the root of the project directory and sourced to the virtual environment as we discussed earlier. We can start the backend server with this command:

    python app.py

The other terminal should be pointed to the spa sub-directory in the project directory, we can start the Vue dev server with this command:

    npm start

The application will be available for testing on this address http://localhost:8080, here’s a display of how it should look and behave when visited:

User-uploaded image: search.gif

Conclusion

It is not uncommon for technical assessment tests to be given during the developer recruitment process. In this article, we have looked at how I completed mine. In the course of the development, we created a CRUD backend server using Flask and developed the Google search clone SPA in Vue using the webpack template that was provided by the Vue cli tool. We also learned how to POST data to a backend server using the JavaScript Fetch API and the conventional XHR object.

I am hopeful that this article has been fun for you to follow along with, if you have any questions, please let me know in the comments.

The source code for this project is available here on GitHub.


Source: Scotch.io

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x