Building a simple data filtering app with Vue js

Vue js, The Progressive JavaScript Framework

Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. On the other hand, Vue is also perfectly capable of powering sophisticated Single-Page Applications when used in combination with modern tooling and supporting libraries.

Vuejs was developed by Evan You, an ex-Google software engineer. Just before launching Vuejs 2.0, he started to work on Vue.js full time and as a result, Vue.js 2 is now significantly lighter, smaller in size and faster. Currently, many popular products use Vuejs to build their user interfaces. Such platforms include Laravel Spark, Grammarly, Statamic, Laracasts and more. There is a comprehensive list of projects using Vuejs on Github. Vuejs 2’s documentation is very detailed, and there is a vibrant community of users. Before we begin lets get familiar with some common concepts in Vue.

KEY CONCEPTS IN VUE

Vuejs 2 is similar to React and Angular 2 in a few ways. There are few key concepts that will help you get started easily. I’ll give a basic overview of these concepts to nourish your understanding of Vuejs. This explanation is all thanks to Prosper Otemuyiwa Tutorial for Vue js 2 Authentication with Auth0. You can find the tutorial here

They are:

  • Directives
  • Components
  • Template/JSX

You can decide to use Vuejs 2 by simply invoking methods on a Vue instance or go the component-composing route.

<div id="app">   
<p>{{ message }}</p>
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello, it is this easy!'
}
})

The result of the code above on the browser will be Hello, it is this easy!. The value of any property in the data object within a new Vue instance will be rendered to the DOM easily. The curly braces are used to display the property on the web page.

Directives

It’s very easy to toggle the display of items on a web page with inbuilt directives such as v-if, v-show like so:

<div id="app">
<p v-if="visible()">{{ message }}</p>
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello, it is this easy!'
},
methods: {
visible: function() {
return true;
}
}
});

If for any reason, the visible function returns false, the paragraph would not be displayed on the web page. What about iterations and loops? Check out the code below

<div id="app">
<ol>
<li v-for="item in items">
{{ item.name }}
</li>
</ol>
</div>
var app = new Vue({
el: '#app',
data: {
items: [
{ name: 'Vue js' },
{ name: 'Angular js' },
{ name: 'React js' }
]
}
});

On the page, it will simply display:

Vue js
Angular js
React js

Components

Vuejs 2 also leverages components. It allows you to build large applications composed of small, self-contained smaller components.

An example of a component is an HTML5 tag, say <header>. A header can have attributes, it can be styled and also possess its own behaviour. In Vuejs 2, you'll be able to build your own custom component by registering it like so:

Vue.component('app-header', {
template: "<li>This is the application's header</li>"
})

Then, you can use it in another component like so:

<div>
<app-header></app-header>
</div>

Vuejs 2 provides some methods that are triggered at various points from creating a component up until the component is destroyed. This is called the Instance Lifecycle, also known as the Component’s Lifecyle. Each Vue instance goes through a series of initialization steps when it is created — for example, it needs to set up data observation, compile the template, mount the instance to the DOM, and update the DOM when data changes. So you can execute custom logic in these hooks. These lifecycle hooks are beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeDestroy and destroyed.

Vuejs 2 Lifecycle hooks

  • beforeCreate() : This method is called synchronously after the Vue instance has just been initialized, before data observation and event/watcher setup.
  • created() : This method is called synchronously after the Vue instance is created. Data observation, computed properties, methods and event callbacks have already been set up at this stage but the mounting phase has not started yet.
  • beforeMount() : This method is called right before the component is mounted. So it is called before the render method is executed.
  • mounted() : This method is called after the component has just been mounted.
  • beforeUpdate() : This method is called when the data changes, before the virtual DOM is re-rendered and patched.
  • updated() : This method is called after a data change causes the virtual DOM to be re-rendered and patched.
  • activated() : This method is called when a kept-alive component is activated.
  • deactivated() : This method is called when a kept-alive component is deactivated.
  • beforeDestroy() : This method is called right before a Vue instance or component is destroyed. At this stage the instance is still fully functional.
  • destroyed() : This method is called after a Vue instance or component has been destroyed. When this hook is called, all directives of the Vue instance have been unbound, all event listeners have been removed, and all child Vue instances have also been destroyed.

Vuejs 2 possess some built-in components such as component, transition, transition-group, keep-alive and slot. You can take advantage of these components in your app. Check out how to use them.

Props

Props is the short form for properties. Properties are attributes of a component. In fact, props are how components talk to each other. A tag in HTML such as <img> has an attribute, a.k.a prop called src that points to the location of an image.

In Vue.js 2, you can pass data from the parent scope into child components. A typical example is this:

Vue.component('tag-list', {
props: ['item'],
template: '<li></li>'
})
var app = new Vue({
el: '#app',
data: {
tagList: [
{ tag: 'x-men' },
{ tag: 'avengers' },
{ tag: 'guardians of the galaxy' }
]
} })
<div id="app">
<ol>
<tag-list v-for="list in tagList" v-bind:item="list"></tag-list>
</ol>
</div>

It will display these items on the web page like so:

x-men
avengers
guardians of the galaxy

Template / JSX

Vue.js 2 uses an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying Vue instance’s data. All Vue.js templates are valid HTML that can be parsed by spec-compliant browsers and HTML parsers.

You can also decide to use JSX. JSX is the combination of HTML and JavaScript code in the same file. The browser is not meant to understand it. It must first be transpiled into standard JavaScript before the browser can understand. An example of JSX usage in Vuejs is:

data: {
text: 'Hello world'
},
render (h) {
return (
<div id='message'>
{ this.text }
</div>
);
}

Now, by default, Vue doesn’t support JSX, but with the help of babel-plugin-transform-vue-jsx we can use JSX with Vue. Oh, the ecosystem should be thanked for this great tool. Whoop! Whoop!

With Vue 2, you can use the render function to create a reactive element. And you can use JSX in it like so:

new Vue({
el: '#app',
data: {
msg: 'Click to see the message.'
},
methods: {
hello () {
alert('This is the message.')
}
},
render: function render(h) {
return (
<span
class=my-class-3
style=
on-click={ this.hello }
>
{ this.msg }
</span>
)
}
});

Can you see the power of JSX manifesting itself in Vue? Awesome, you can check out more information on JSX in Vue here.

Next, let’s build an application with Vuejs 2.

A SIMPLE DATA FILTERING APP

Note that for this app, we are going to be using definitive data, meaning we already know what we are filtering so that app will be tailored based on the data we have. The main aim of this tutorial is to touch basic major concepts in Vue js. For this we are going to be using vue-cli. Vue.js provides an official CLI for quickly scaffolding ambitious Single Page Applications. It provides batteries-included build setups for a modern frontend workflow. It takes only a few minutes to get up and running with hot-reload, lint-on-save, and production-ready builds. First of all make sure you have Node js installed on your machine. You can get that here.

Next create a folder for your Vue apps and open a terminal on it. Install vue-cli globally using this command

npm install --global vue-cli

Then use vue-cli to scaffold your application

vue init webpack filter-app

The set up should ask you a couple of questions. Decline for tests . We do not need them for this tutorial. You can learn about them later. Then navigate into your project folder and serve the app

cd filter-app
npm run dev

The initial setup for the project would have asked you if you wanted to do an npm install , but just incase the serving of the app gives an error just enter npm install as a command. After the app is served, it should tell you the url to access the app. Just go to the url on your browser and you should see the app.

Let’s check out the structure of our newly scaffolded app.

filter-app/
build/ - All build files are here
config/ - All environment config files are here
node_modules/ - All the packages required for the vuejs app resides here
src/
- assets - All assets reside here
- components - All our Vue components resides here
- router - Our router is defined here
- App.vue - The Parent component
- main.js - Starting point for our app where the router, template and App component are bound to the root app div
static/ - contains static files
.babelrc
.editorconfig
.eslintignore
.eslintrc.js
.gitignore
.postcssrc.js
index.html - Index file that declares the root div where the App component is been bound to
package.json - File that contains the names of all the packages residing in node_modules folder
README.md
node_modules/ - All the packages required for the react app resides here
package.json - File that contains the names of all the packages residing in node_modules folder

We will work with this structure but make some few modifications.

Create a data folder in the src folder and create a file in it named data.js. Open the file up and add this code to it

const data = [
{
name: 'vue js',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501994/vue-logo_uoxef2.png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ]
},
{
name: 'react js',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501994/react-logo_n8nigl.png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ]
},
{
name: 'angular js',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501990/angular-logo_wnra1m.png',
stack: [ 'framework', 'frontend', 'web', 'mobile' ]
},
{
name: 'javascript',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/javascript-logo_kz7zja.png',
stack: [ 'language', 'web', 'mobile' ]
},
{
name: 'php',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501993/php-logo_hojx5k.png',
stack: [ 'language', 'web' ]
},
{
name: 'java',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501991/java-logo_doy3o0.png',
stack: [ 'language', 'web', 'mobile' ]
},
{
name: 'python',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501993/python-logo_mjaxc1.png',
stack: [ 'language', 'web' ]
},
{
name: 'go',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501991/go-logo_avni2x.png',
stack: [ 'language', 'web' ]
},
{
name: 'html',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/html-logo_oe7cb4.png',
stack: [ 'language', 'web' ]
},
{
name: 'css',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501991/css-logo_hsgsej.png',
stack: [ 'language', 'web' ]
},
{
name: 'scss',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501993/scss-logo_z71exc.png',
stack: [ 'language', 'web' ]
},
{
name: 'kotlin',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501993/kotlin-logo_dnjpni.png',
stack: [ 'language', 'mobile' ]
},
{
name: 'android',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501990/android-logo_uqw54t.png',
stack: [ 'language', 'mobile' ]
},
{
name: 'swift',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501994/swift-logo_bdjes1.png',
stack: [ 'language', 'mobile' ]
},
{
name: 'ionic',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/ionic-logo_bc7d4u.png',
stack: [ 'framework', 'hybrid', 'mobile', 'web' ]
},
{
name: 'flutter',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501991/flutter-logo_co1o4s.png',
stack: [ 'framework', 'hybrid', 'mobile' ]
},
{
name: 'node js',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/nodejs-logo_w8vt1c.png',
stack: [ 'framework', 'frontend', 'backend', 'web' ]
},
{
name: 'laravel',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/laravel-logo_e3gsn1.png',
stack: [ 'framework', 'frontend', 'backend', 'web' ]
},
{
name: 'django',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501990/django-logo_vutkvt.png',
stack: [ 'framework', 'frontend', 'backend', 'web' ]
},
{
name: 'graphql',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/graphql-logo_mdrics.png',
stack: [ 'backend' ]
},
{
name: 'mySQL',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501993/mysql-logo_g87g6v.png',
stack: [ 'database' ]
},
{
name: 'mongo DB',
logo: 'http://res.cloudinary.com/johnayeni/image/upload/v1524501992/mongo-logo_ryaupe.png',
stack: [ 'database' ]
}
];
export default data;

This will serve as our data for the app

Now to create the components we need. Delete the HelloWorld.vue file in the components/src folder and add create this two files SearchPage.vue and ItemCard.vue . Please note that file names are case sensitive.

Also for the design, we are going to be using bootstrap 4 and Fontawesome icons. So we are just going to add CDN links to the index.html file to which our app builds to which is located in the root folder. So our index.html file should look like this

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Filter App</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.10/css/all.css" integrity="sha384-+d0P83n9kaQMCwj8F4RJB66tzIwOKmrdb46+porD/OvrJ+37WqIM7UoBtwHO6Nlg" crossorigin="anonymous">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
</body>
</html>

Next to the routing. For this app we only have one route which is the root route(trailing slash route). The only thing we have to change though is the component in which that route renders which will be our SearchPage.vue component. If you want to know more about routing in Vue js, Read more on it here. Now open up the index.js file in the router folder and edit to this

import Vue from 'vue';
import Router from 'vue-router';
import SearchPage from '@/components/SearchPage';
Vue.use(Router);
export default new Router({
  routes: [
    {
        path: '/',
        name: 'SearchPage',
        component: SearchPage
    }
 ]
});

What we have just done is to set the app to render the Search Page component on when on the root route. Now to the main thing. Making our search page. before we do that, remember the Itemcard.vue file we created. This component is meant to be for each item we display from our data. For each item we are going to make them cards. Ad this code to the ItemCard.vue file

<template>
  <div class="card p-3">
    <div class="text-center">
      <img class="img-fluid" :src="item.logo" width="100" alt="Card   image cap">
      <div class="card-body">
        <h5 class="card-title">{{ item.name | capitalize }} </h5>
      </div>
      <div>
    <span v-for="(group, index) in item.stack" :key="index" :class="`badge badge-${tags[group]}`">{{ group }}</span>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
// list of tags to giving each stack a different color
      tags: {
        language: 'light',
        framework: 'dark',
        frontend: 'success',
        backend: 'danger',
        mobile: 'warning',
        web: 'secondary',
        hybrid: 'info',
        database: 'danger'
      }
    };
  },
  filters: {
// this filter will can be used to capitalize a word
    capitalize: item => {
      return item.toUpperCase();
    }
  },
  props: {
// this component expects a prop of type object
    item: {
      type: Object,
      required: true
    }
  }
};
</script>

Next is to create our search page component. Now basically, let me give tell you the list of things we need this component to do and have

To have
- A search bar where a data keyword can be typed in
- A group of checkboxes for filters
- Display of all data depending on the the search key word and filters selected
To do
- To display all data available on initial mounting of the component
- To changed data being displayed based on search keyword and filters selected

Now that we know what we need we can go ahead to write the code we need for it. This code will go to the SearchPage.vue file we created in the src/components folder

<template>
  <div class="container-fluid">
    <div class="search-wrapper">
    <!-- the search bar form -->
      <form v-on:submit="getfilteredData">
        <div class="form-row">
          <div class="col-10">
            <input type="text" class="form-control" placeholder="Enter key word  ..." v-model="search" v-on:keyup="getfilteredData">
          </div>
          <div class="col-2">
            <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
          </div>
        </div>
      </form>
      <!-- check boxes -->
      <div id="checkboxes">
      <div v-for="(stack,index) in stacks" :key="index" class="form-check form-check-inline">
        <input class="form-check-input" type="checkbox"  v-model="stack.checked" v-on:change="getfilteredData">
        <label class="form-check-label">
        {{ stack.value }}
        </label>
      </div>
    </div>
  </div>
  <!-- end of checkboxes -->
  <div class="card-columns">
    <!-- iterate data -->
    <item-card v-for="(item, index) in filteredData" :key="index" :item="item"></item-card>
   </div>
  </div>
</template>
<script>
import ItemCard from './ItemCard';
import data from '../data/data';
export default {
  name: 'SearchPage',
  components: {
    'item-card': ItemCard
  },
  computed: {
    selectedFilters: function() {
      let filters = [];
      let checkedFiters = this.stacks.filter(obj => obj.checked);
      checkedFiters.forEach(element => {
        filters.push(element.value);
      });
      return filters;
    }
  },
  data() {
    return {
      filteredData: [],
      search: '',
      stacks: [
      {
        checked: false,
        value: 'language'
      },
      {
        checked: false,
        value: 'framework'
      },
      {
        checked: false,
        value: 'frontend'
        },
      {
        checked: false,
        value: 'backend'
      },
      {
        checked: false,
        value: 'mobile'
      },
      {
        checked: false,
        value: 'web'
      },
      {
        checked: false,
        value: 'hybrid'
      },
      {
        checked: false,
        value: 'database'
      }
      ]
    };
  },
  methods: {
    getfilteredData: function() {
      this.filteredData = data;
      let filteredDataByfilters = [];
      let filteredDataBySearch = [];
      // first check if filters where selected
      if (this.selectedFilters.length > 0) {
        filteredDataByfilters= this.filteredData.filter(obj => this.selectedFilters.every(val => obj.stack.indexOf(val) >= 0));
        this.filteredData = filteredDataByfilters;
      }
      // then filter according to keyword, for now this only affects the name attribute of each data
      if (this.search !== '') {
        filteredDataBySearch = this.filteredData.filter(obj => obj.name.indexOf(this.search.toLowerCase()) >= 0);
        this.filteredData = filteredDataBySearch;
      }
    }
  },
  mounted() {
    this.getfilteredData();
  }
};
</script>

So basically the way this component works is that when its mounted, it fetches data which will return all data available since there is no search key word or any filters selected. Each checkbox input is tied down to to a model, which in this case is one stack. Each time any checkbox changes it triggers the getfilteredData function. Same also with the search bar. The getfilteredData function is also triggered on keyup.

One last thing, open up your App.vue file in the src folder and edit its contents to this

<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<script>
export default {
  name: 'App'
};
</script>
<style>
#app {
  margin-top: 50px;
}
.search-wrapper {
  margin: 20px;
}
.badge {
  border-radius: 0;
}
</style>

Now make sure your app is still serving, if not fire it up again and check your browser. Everything should work fine and you should have this

Cool right ??

Play around with the app. You can make modifications as you like to add more cool features to the search and make it more dynamic. Get the full code of the app here on Github. We’re done, “adios amigos”.