Building a Mini Invoicing App with Vue and Node

Building a Mini Invoicing App with Vue and Node

In this article, we are going to look at how to build a mini invoicing application using Vue and NodeJS. This application will perform simple function such as - creating an invoice, sending an invoice, editing and deleting an invoice.

Building a Mini Invoicing App with Vue and Node : Part 1 - Database and API


To get paid for goods and services provided, companies/freelancers need to send invoices to their customers informing them of the services that they will be charged for. Back then, people had paper invoices which they gave to the customers when they contact them for their services. Right now, with the advent and advancement of technology, people are now able to send electronic invoices to their customers. 

Requirements

Table of Contents

  • Requirements
  • Setting Up Server
  • Creating And Connecting To Database Using SQLite
  • Creating Application Routes
  • Conclusion

To follow through the article adequately, you’ll need the following:

  • Node installed on your machine
  • Node Package Manager (NPM) installed on your machine

To verify your installation, run the following :

    node --version
    npm --version

If you get version numbers as results then you’re good to go.

Setting Up Server

Now that we have the requirements all set, the next thing to do is to create the backend server for the application. The backend server will simply:

  • Maintain the database connection

Now, let’s get to creating the server. Create a folder to house the new project and initialize it as a node project:

    mkdir invoicing-app
    cd invoicing-app && npm init

For the server to function appropriately, there are some node packages that need to be installed, you can install them by running the command :

    npm install --save express body-parser connect-multiparty sqlite3 bluebird path umzug bcrypt
  • bcrypt to hash user passwords
  • express to power our web application
  • sqlite3 to create and maintain the database
  • path to resolve file paths within our application
  • bluebird to use Promises when writing migrations
  • umzug as a task runner to run our database migrations
  • body-parser and connect-multiparty to handle incoming form requests

Now, go ahead to create a server.js file that will house the application logic:

    touch server.js

In the server.js, Import the necessary modules and create an express app:

    // server.js
const express = require('express')
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const PORT = process.env.PORT || 3128;

const app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

[...]

then create a simple / route to test our server works:

    // server.js
[...]

app.get('/', function(req,res){
    res.send("Welcome to Invoicing App");
});

app.listen(PORT, function(){
    console.log(`App running on localhost:${PORT}`);
});

app.listen() tells the server the port to listen to for incoming routes.

To start the server, run the following in your project directory:

    node server

Your application begins to listen to incoming requests.


Creating And Connecting To Database Using SQLite

For an invoicing application, a database is needed to store the existing invoices. SQLite is going to be our database client of choice for this application.

To create a database folder, create a file for your database in the folder:

    mkdir database
cd database && touch InvoicingApp.db

In the database directory, run the sqlite3 client and then open InvoicingApp.db database

    $ invoicing-app/database/   sqlite3
> .open InvoicingApp.db

Now that the database to be used has been selected, next thing is to create the needed tables.

Database Structure For this application, there are 3 tables needed:

  • Users - This will contain the user data (id, name, email, company_name, password)
  • Invoices - Store data for an invoice (id, name, paid, user_id)
  • Transactions - Singular transactions that come together to make an invoice (name, price, invoice_id)

Since the necessary tables have been identified, the next step is to run the queries to create the tables.

Writing Migrations Migrations are used to keep track of changes in a database as the application grows. To do this, create a migrations folder in the database directory.

    mkdir migrations

This will house all the migration files.

Now, create a 1.0.js file in the migrations folder. This naming convention is to keep track of the newest changes.

    cd migations && touch 1.0.js

In the 1.0.js file, you first import the node modules:

    // database/migrations 1.0.js
"use strict";
const Promise = require("bluebird");
const sqlite3 = require("sqlite3");
const path = require('path');

[...]

Then, the idea now is to export an up function that will be executed when the migration file is run and a down function to reverse the changes to the database.

    // database/migrations/1.0.js

[...]
module.exports = {
  up: function() {
    return new Promise(function(resolve, reject) {
      /* Here we write our migration function */
      let db = new sqlite3.Database('./database/InvoicingApp.db');
      //   enabling foreign key constraints on sqlite db
      db.run(`PRAGMA foreign_keys = ON`);

      [...]

In the up function, connection is first made to the database and then, foreign keys are enabled on the sqlite database

In SQLite, foreign keys are disabled by default to allow for backwards compatibility so, the foreign keys have to be enabled on every connection

After successful connection, the queries to create the tables are then specified

    // database/migrations/1.0.js
[...]
db.serialize(function() {
db.run(CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, email TEXT, company_name TEXT, password TEXT ));

        db.run(`CREATE TABLE invoices (
          id INTEGER PRIMARY KEY,
          name TEXT,
          user_id INTEGER,
          paid NUMERIC,
          FOREIGN KEY(user_id) REFERENCES users(id)
        )`);

        db.run(`CREATE TABLE transactions (
          id INTEGER PRIMARY KEY,
          name TEXT,
          price INTEGER,
          invoice_id INTEGER,
          FOREIGN KEY(invoice_id) REFERENCES invoices(id)
        )`);
      });
      db.close();
    });
  },
  [...]

The serialize() is used to specify that the queries should be run sequentially and not simultaneously

Afterwards, the queries to reverse the changes are also specified in the down() function below

    // database/migrations/1.0.js
[...]

  down: function() {
    return new Promise(function(resolve, reject) {
      /* This runs if we decide to rollback. In that case we must revert the `up` function and bring our database to it's initial state */
      let db = new sqlite3.Database("./database/InvoicingApp.db");
      db.serialize(function() {
        db.run(`DROP TABLE transactions`);
        db.run(`DROP TABLE invoices`);
        db.run(`DROP TABLE users`);
      });
      db.close();
    });
  }
};

Running Migrations Once the migration files have been created, the next step is running them to effect the changes in the database. To do this, create a scripts folder from the root of your application and then create a file called migrate.js.

    mkdir scripts
cd scripts && touch migrate.js

The migrate.js file will look like this:

    // scripts/migrate.js

const path = require("path");
const Umzug = require("umzug");

let umzug = new Umzug({
  logging: function() {
    console.log.apply(null, arguments);
  },
  migrations: {
    path: "./database/migrations",
    pattern: /\.js$/
  },
  upName: "up",
  downName: "down"
});

[...]

First, the needed node modules are imported and then, a new umzug object is created with the following configurations. The path and pattern of the migrations scripts are also specified. To learn more about the configurations, head over here

To also give some verbose feedback, create a function to log events as shown below and then finally execute the up function to run the database queries specified in the migrations folder.

    // scripts/migrate.js
[...]

function logUmzugEvent(eventName) {
  return function(name, migration) {
    console.log(`${name} ${eventName}`);
  };
}

// using event listeners to log events
umzug.on("migrating", logUmzugEvent("migrating"));
umzug.on("migrated", logUmzugEvent("migrated"));
umzug.on("reverting", logUmzugEvent("reverting"));
umzug.on("reverted", logUmzugEvent("reverted"));

// this will run your migrations
umzug.up().then(console.log("all migrations done"));

Now, to execute the script, go to your terminal and in the root directory of your application, run:

    > ~/invoicing-app node scripts/migrate.js up
all migrations done
== 1.0: migrating =======
1.0 migrating
Creating Application Routes

Now that the database is adequately set up, the next thing is to go back to the server.js file and create the application routes. For this application, the following routes will be made available:

Registering a New User To register a new user, a post request will be made to the /register route of our server. This route will look like this :

    // server.js
[...]
const bcrypt = require('bcrypt')
const saltRounds = 10;
[...]

app.post('/register', function(req, res){
    // check to make sure none of the fields are empty
    if( isEmpty(req.body.name)  || isEmpty(req.body.email) || isEmpty(req.body.company_name) || isEmpty(req.body.password) ){
        return res.json({
            'status' : false,
            'message' : 'All fields are required'
        });
    }
    // any other intendend checks

    [...]

A check is made to see if any of the fields are empty. If no fields are empty and if the data sent matches all the specification. If an error occurs, an error message is sent to the user as a response. If not, the password is hashed and the data is then stored in the database and a response is sent to the user informing them that they are registered.

    // server.js
bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
let db = new sqlite3.Database("./database/InvoicingApp.db");
let sql = INSERT INTO users(name,email,company_name,password) VALUES('${ req.body.name }','${req.body.email}','${req.body.company_name}','${hash}');
db.run(sql, function(err) {
if (err) {
throw err;
} else {
return res.json({
status: true,
message: "User Created"
});
}
});
db.close();
});
});

When a sample request is made from Postman, the result below is obtained :

Logging in a New User Now, if an existing user tries to log in to the system using the /login route, they need to provide their email address and password. Once they do that, the route handles the request as follows :

    // server.js
[...]

app.post("/login", function(req, res) {
  let db = new sqlite3.Database("./database/InvoicingApp.db");
  let sql = `SELECT * from users where email='${req.body.email}'`;
  db.all(sql, [], (err, rows) => {
    if (err) {
      throw err;
    }
    db.close();
    if (rows.length == 0) {
      return res.json({
        status: false,
        message: "Sorry, wrong email"
      });
    }

  [...]

A query is made to the database to fetch the record of the user with a particular email. If the result returns an empty array, then it means that the user doesn’t exist and a response is sent informing the user of the error.

If the database query returned user data, further check is made to see if the password entered matches that password in the database. If it does, then a response is sent with the user data.

    // server.js
[...]
let user = rows[0];
let authenticated = bcrypt.compareSync(req.body.password, user.password);
delete user.password;
if (authenticated) {
return res.json({
status: true,
user: user
});
}
return res.json({
status: false,
message: "Wrong Password, please retry"
});
});
});

[...]

When the route is tested from Postman, you get this result:

Creating a new Invoice The /invoice route handles the creation of an invoice. Data passed to the route will include the following:

  • User ID, Name of the invoice and invoice status.
  • Singular Transactions to make up the invoice.

The server handles the request as follows:

    // server.js
[...]
app.post("/invoice", multipartMiddleware, function(req, res) {
// validate data
if (isEmpty(req.body.name)) {
return res.json({
status: false,
message: "Invoice needs a name"
});
}
// perform other checks

  [...]

First, the data sent to the server is validated and then, a connection is made to the database for the subsequent queries.

    // server.js
[...]
// create invoice
let db = new sqlite3.Database("./database/InvoicingApp.db");
let sql = INSERT INTO invoices(name,user_id,paid) VALUES( '${req.body.name}', '${req.body.user_id}', 0 );
[...]

The insert query needed to create the invoice is written and then, it is executed. Afterwards, the singular transactions are inserted into the transactions table with the invoice_id as foreign key to reference them.

    // server.js
[...]
db.serialize(function() {
db.run(sql, function(err) {
if (err) {
throw err;
}
let invoice_id = this.lastID;
for (let i = 0; i < req.body.txn_names.length; i++) {
let query = INSERT INTO transactions(name,price,invoice_id) VALUES( '${req.body.txn_names[i]}', '${req.body.txn_prices[i]}', '${invoice_id}' );
db.run(query);
}
return res.json({
status: true,
message: "Invoice created"
});
});
});
[...]

Once this is executed, the invoice is successfully created:

On checking the SQLite database, the following result is obtained:

    sqlite> select * from invoices;
1|Test Invoice New|2|0
sqlite> select * from transactions;
1|iPhone|600|1
2|Macbook|1700|1

Fetching All Invoices

Now, when a user wants to see all the created invoices, the client will make a GET request to the /invoice/user/:id route. The user id is passed as a route parameter. The request is handled as follows:

    // index.js
[...]
app.get("/invoice/user/:user_id", multipartMiddleware, function(req, res) {
let db = new sqlite3.Database("./database/InvoicingApp.db");
let sql = SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}';
db.all(sql, [], (err, rows) => {
if (err) {
throw err;
}
return res.json({
status: true,
transactions: rows
});
});
});

[...]

A query is run to fetch all the invoices and the transactions related to the invoice belonging to a particular user.

Fetching Single Invoice To fetch a specific invoice, the a GET request is with the user_id and invoice_id to the /invoice/user/{user_id}/{invoice_id} route. The request is handles as follows

    // index.js
[...]

app.get("/invoice/user/:user_id/:invoice_id", multipartMiddleware, function(req, res) {
  let db = new sqlite3.Database("./database/InvoicingApp.db");
  let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${
    req.params.user_id
  }' AND invoice_id='${req.params.invoice_id}'`;
  db.all(sql, [], (err, rows) =&gt; {
    if (err) {
      throw err;
    }
    return res.json({
      status: true,
      transactions: rows
    });
  });
});

// set application port
[...]

A query is run to fetch a single invoice and the transactions related to the invoice belonging to the user.

Running the request on Postman will give you the result below :


Conclusion

In this part of the series, we walked though how to get up your server with all the needed routes for the mini invoicing application. In the next part of this series, we will look at how to Create the Interface for the invoicing application using Vue. Here’s a link to the full Github repository.

Building a Mini Invoicing App with Vue and NodeJS : Part 2 - User Interface


In this part, let’s take a look at how to build the part of the application users will interact with, the user interface.


Prerequisites

To follow through this article, you’ll need the following:

Table of Contents

  • Prerequisites
  • Installing Vue
  • Creating the Project
  • Configuring Vue Router
  • Creating Components
  • Conclusion
  • Node installed on your machine
  • NPM installed on your machine
  • Have read through the first part of this series.


Installing Vue

To build the frontend of this application, we’ll be using Vue. Vue is a progressive JavaScript framework used for building useful and interactive user interfaces. To install vue, run the following command:

npm install -g vue-cli

To confirm your Vue installation, run the command:

vue --version

If you get the version number as your result then you’re good to go.


Creating the Project

To create a new project with Vue, run the following command and then follow the prompt:

vue init webpack invoicing-app-frontend
If you have followed the first part of the series, you can run this command in the same project directory. If not the command will create the project in your existing directory.

This creates a sample Vue project that we’ll build upon in this article.

Installing Node Modules For the frontend of this invoicing application, a lot of requests are going to be made to the backend server. To do this, we’ll make use of axios. To install axios, run the command in your project directory:

npm install axios --save

Adding Bootstrap To allow us get some default styling in our application, We’ll make use of Bootstrap. To add it to your application, add the following to the index.html file in the project:

    // index.html
[...]
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
[...]
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"crossorigin="anonymous"></script>
[...]
Configuring Vue Router

For this application, we are going to have 2 major routes:

  • / - To render the login page
  • /dashboard - To render the user dashboard

To configure these routes, open the src/router/index.js and update it to look like this:

// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import SignUp from '@/components/SignUp'
import Dashboard from '@/components/Dashboard'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'SignUp',
component: SignUp
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard
},
]
})

This specifies the components that should be displayed to the user when they visit your application.


Creating Components

One of the major selling points of Vue is the component structure. Components allows the frontend of your application to be more modular and reusable. For this application, we’ll have the following components:

  • SignUp/SignIn Component
  • Header Component
  • Navigation Component
  • Dashboard Component
  • View Invoices Component
  • Create Invoice Component
  • Single Invoice Component

Navigation Component This is the sidebar that will house the links of different actions. Create a new component in the /src/components directory:

touch SideNav.vue

Now, edit the SideNav.vue like this:

// src/components/SideNav.vue
<script>
export default {
name: "SideNav",
props: ["name", "company"],
methods: {
setActive(option) {
this.$parent.$parent.isactive = option;
},
openNav() {
document.getElementById("leftsidenav").style.width = "20%";
},
closeNav() {
document.getElementById("leftsidenav").style.width = "0%";
}
}
};
</script>

[...]

The component is created with two props : first the name of the user and second, the name of the company. The two methods are to add the collapsible functionality to the sidebar. The setActive method will update the component calling the parent of the SideNav component, in this case Dashboard, when a user clicks on a navigation link.

The component has the following template:

// src/components/SideNav.vue
[...]
<template>
<div>
<span style="font-size:30px;cursor:pointer" v-on:click="openNav">&#9776;</span>
<div id="leftsidenav" class="sidenav">
<p style="font-size:12px;cursor:pointer" v-on:click="closeNav"><em>Close Nav</em></p>
<p><em>Company: {{ company }} </em></p>
<h3>Welcome, {{ name }}</h3>
<p class="clickable" v-on:click="setActive('create')">Create Invoice</p>
<p class="clickable" v-on:click="setActive('view')">View Invoices</p>
</div>
</div>
</template>
[...]
Styling for the component can be obtained from the Github repository.

Header Component The header component is simple, it displays the name of the application and side bar if a user is signed in. Create a Header.vue file in the src/components directory:

touch Header.vue

The component file will look like this:

// src/components/Header.vue
<template>
<nav class="navbar navbar-light bg-light">
<template v-if="user != null">
<SideNav v-bind:name="user.name" v-bind:company="user.company_name"/>
</template>
<span class="navbar-brand mb-0 h1">{{title}}</span>
</nav>
</template>
<script>
import SideNav from './SideNav'
export default {
name: "Header",
props : ["user"],
components: {
SideNav
},
data() {
return {
title: "Invoicing App",
};
}
};
</script>

The header component has a single prop called user. This prop will be passed by any component that will use the header component. In template for the header, the SideNav component created earlier is imported and conditional rendering is used to determine if the SideNav should be displayed or not.

SignUp/SignIn Component This component has one job, to house the signup form and sign in form. Create a new file in /src/components directory.

touch SignIn.vue

Now, the component to register/login a user is a little more complex than the two earlier components. We need to have the Login and Register forms on the same page. Let’s take a look at how to achieve this.

First, create the component:

// src/components/SignIn.vue
<script>
import Header from "./Header";
import axios from "axios";
export default {
name: "SignUp",
components: {
Header
},
data() {
return {
model: {
name: "",
email: "",
password: "",
c_password: "",
company_name: ""
},
loading: "",
status: ""
};
},
[...]

The Header component is imported and the data properties of the components are also specified. Next, create the methods to handle what happens when data is submitted:

// src/components/SignIn.vue
[...]
methods: {
validate() {
// checks to ensure passwords match
if( this.model.password != this.model.c_password){
return false;
}
return true;
},
[...]

The validate method performs checks to make sure the data sent by the user meets our requirements.

// src/components/SignIn.vue
[...]
register() {
const formData = new FormData();
let valid = this.validate();
if(valid){
formData.append("name", this.model.name);
formData.append("email", this.model.email);
formData.append("company_name", this.model.company_name);
formData.append("password", this.model.password);

this.loading = "Registering you, please wait";
// Post to server
axios.post("http://localhost:3128/register", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: "Dashboard",
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
}else{
alert("Passwords do not match");
}
},
[...]

The register method of the component handles the action when a user tries to register a new account. First, the data is validated using the validate method. Then If all criteria are met, the data is prepared for submission using the formData.

We’ve also defined the loading property of the component to let the user know when their form is being processed. Finally, a POST request is sent to the backend server using axios. When a response is received from the server with a status of true, the user is directed to the dashboard else, an error message is displayed to the user.

   // src/components/SignIn.vue
[...]
login() {
const formData = new FormData();
formData.append("email", this.model.email);
formData.append("password", this.model.password);
this.loading = "Signing in";
// Post to server
axios.post("http://localhost:3128/login", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: "Dashboard",
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
}
}
};
</script>

Just like the register method, the data is prepared and sent over to the backend server to authenticate the user. If the user exists and the details match, the user is directed to their dashboard.

The Vue router was used here to direct the user to a different view. This will be visited more in-depth as we move on

Now, let’s also take a look at the template for the signup component:

   // src/components/SignUp.vue
<template>
[...]
<div class="tab-pane fade show active" id="pills-login" role="tabpanel" aria-labelledby="pills-login-tab">
<div class="row">
<div class="col-md-12">
<form @submit.prevent="login">
<div class="form-group">
<label for="">Email:</label>
<input type="email" required class="form-control" placeholder="eg [email protected]" v-model="model.email">
</div>

      &lt;div class="form-group"&gt;
          &lt;label for=""&gt;Password:&lt;/label&gt;
          &lt;input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password"&gt;
      &lt;/div&gt;

      &lt;div class="form-group"&gt;
          &lt;button class="btn btn-primary" &gt;Login&lt;/button&gt;
          {{ loading }}
          {{ status }}
      &lt;/div&gt;
  &lt;/form&gt; 

</div>
</div>
</div>
[...]

The login in form is shown above and the input fields are linked to the respective data properties specified when the components were created. When the submit button of the form is clicked, the login method of the component is called.

Usually, when the submit button of a form is clicked, the form is submitted via a GET or POST request but instead of using that, we added this when creating the form to override the default behavior and specify that the login function should be called.

<form @submit.prevent="login">

The register form also looks like this:

// src/components/SignIn.vue
[...]
<div class="tab-pane fade" id="pills-register" role="tabpanel" aria-labelledby="pills-register-tab">
<div class="row">
<div class="col-md-12">
<form @submit.prevent="register">
<div class="form-group">
<label for="">Name:</label>
<input type="text" required class="form-control" placeholder="eg Chris" v-model="model.name">
</div>
<div class="form-group">
<label for="">Email:</label>
<input type="email" required class="form-control" placeholder="eg [email protected]" v-model="model.email">
</div>

      &lt;div class="form-group"&gt;
          &lt;label for=""&gt;Company Name:&lt;/label&gt;
          &lt;input type="text" required class="form-control" placeholder="eg Chris Tech" v-model="model.company_name"&gt;
      &lt;/div&gt;
      &lt;div class="form-group"&gt;
          &lt;label for=""&gt;Password:&lt;/label&gt;
          &lt;input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password"&gt;
      &lt;/div&gt;
      &lt;div class="form-group"&gt;
          &lt;label for=""&gt;Confirm Password:&lt;/label&gt;
          &lt;input type="password" required class="form-control" placeholder="Confirm Passowrd" v-model="model.confirm_password"&gt;
      &lt;/div&gt;
      &lt;div class="form-group"&gt;
          &lt;button class="btn btn-primary" &gt;Register&lt;/button&gt;
          {{ loading }}
          {{ status }}
      &lt;/div&gt;
  &lt;/form&gt;

</div>
</div>
[...]
</template>

The @submit.prevent is also used here to call the register method when the submit button is clicked.

Now, when you run your development server using the command:

npm run dev

Visit localhost:8080/ on your browser and you get the following result:

Dashboard Component Now, the Dashboard component will be displayed when the user gets routed to the /dashboard route. It displays the Header and the CreateInvoice component by default. Create the Dashboard.vue file in the src/components directory

touch Dashboard.vue

Edit the file to look like this:

// src/component/Dashboard.vue
<script>
import Header from "./Header";
import CreateInvoice from "./CreateInvoice";
import ViewInvoices from "./ViewInvoices";
export default {
name: "Dashboard",
components: {
Header,
CreateInvoice,
ViewInvoices,
},
data() {
return {
isactive: 'create',
title: "Invoicing App",
user : (this.$route.params.user) ? this.$route.params.user : null
};
}
};
</script>
[...]

Above, the necessary components are imported and are rendered based on the template below:

// src/components/Dashboard.vue
[...]
<template>
<div class="container-fluid" style="padding: 0px;">
<Header v-bind:user="user"/>
<template v-if="this.isactive == 'create'">
<CreateInvoice />
</template>
<template v-else>
<ViewInvoices />
</template>
</div>
</template>
We will look at how to create the CreateInvoice and ViewInvoices components later in the article.

Create Invoice Component The CreateInvoice component contains the form needed to create a new invoice. Create a new file in the src/components directory:

    touch CreateInvoice.vue

Edit the CreateInvoice component to look like this:

// src/components/CreateInvoice.vue

<template>
<div>
<div class="container">
<div class="tab-pane fade show active">
<div class="row">
<div class="col-md-12">
<h3>Enter Details below to Create Invoice</h3>
<form @submit.prevent="onSubmit">
<div class="form-group">
<label for="">Invoice Name:</label>
<input type="text" required class="form-control" placeholder="eg Seller's Invoice" v-model="invoice.name">
</div>
<div class="form-group">
<label for="">Invoice Price:</label><span> $ {{ invoice.total_price }}</span>
</div>
[...]

We create a form that accepts the name of the invoice and displays the total price of the invoice. The total price is obtained by summing up the prices of individual transactions for the invoice.

Let’s take a look at how transactions are added to the invoice:

// src/components/CreateInvoice.vue
[...]
<hr />
<h3> Transactions </h3>
<div class="form-group">
<label for="">Add Transaction:</label>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#transactionModal">
+
</button>
<!-- Modal -->
<div class="modal fade" id="transactionModal" tabindex="-1" role="dialog" aria-labelledby="transactionModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Add Transaction</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="">Transaction name</label>
<input type="text" id="txn_name_modal" class="form-control">
</div>
<div class="form-group">
<label for="">Price ($)</label>
<input type="numeric" id="txn_price_modal" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Discard Transaction</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" v-on:click="saveTransaction()">Save transaction</button>
</div>
</div>
</div>
</div>
</div>
[...]

A button is displayed for the user to add a new transaction. When the button is clicked, a modal is shown to the user to enter the details of the transaction. When the Save Transaction button is clicked, a method adds it to the existing transactions.

// src/components/CreateInvoice.vue
[...]
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Transaction Name</th>
<th scope="col">Price ($)</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<template v-for="txn in transactions">
<tr :key="txn.id">
<th>{{ txn.id }}</th>
<td>{{ txn.name }}</td>
<td>{{ txn.price }} </td>
<td><button type="button" class="btn btn-danger" v-on:click="deleteTransaction(txn.id)">X</button></td>
</tr>
</template>
</tbody>
</table>
</div>

<div class="form-group">
<button class="btn btn-primary" >Create Invoice</button>
{{ loading }}
{{ status }}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
[...]

The existing transactions are displayed in a tabular format. When the X button is clicked, the transaction in question is deleted from the transaction list and the Invoice Price is recalculated. Finally, the Create Invoice button triggers a function that then prepares the data and sends it to the backend server for creation of the invoice.

Let’s also take a look at the component structure of the Create Invoice component:

// src/components/CreateInvoice.vue
[...]
<script>
import axios from "axios";
export default {
name: "CreateInvoice",
data() {
return {
invoice: {
name: "",
total_price: 0
},
transactions: [],
nextTxnId: 1,
loading: "",
status: ""
};
},
[...]

First, we defined the data properties for the component. The component will have an invoice object containing the invoice name and total_price. It’ll also have an array of transactions with an index “nextTxnId". This will keep track of the transactions and variables to send status updates to the user (loading, status)

// src/components/CreateInvoice.vue
[...]
methods: {
saveTransaction() {
// append data to the arrays
let name = document.getElementById("txn_name_modal").value;
let price = document.getElementById("txn_price_modal").value;

  if( name.length != 0 &amp;&amp; price &gt; 0){
    this.transactions.push({
      id: this.nextTxnId,
      name: name,
      price: price
    });
    this.nextTxnId++;
    this.calcTotal();
    // clear their values
    document.getElementById("txn_name_modal").value = "";
    document.getElementById("txn_price_modal").value = "";
  }
},
[...]

The methods for the CreateInvoice component are also defined. The saveTransaction() method takes the values in the transaction form modal and then adds it to the transaction list. The deleteTransaction() method deletes an existing transaction object from the list of transactions while the calcTotal() method recalculates the total invoice price when a new transaction is added or deleted.

// src/components/CreateInvoice.vue
[...]
deleteTransaction(id) {
let newList = this.transactions.filter(function(el) {
return el.id !== id;
});
this.nextTxnId--;
this.transactions = newList;
this.calcTotal();
},
calcTotal(){
let total = 0;
this.transactions.forEach(element => {
total += parseInt(element.price);
});
this.invoice.total_price = total;
},
[...]

Finally, the onSubmit() method will submit the form to the backend server. In the method, we’ll use formData and axios to send the requests. The transactions array containing the transaction objects is split into two different arrays. One array to hold the transaction names and the other to hold the transaction prices. The server then attempts to process the request and send back a response to the user.

onSubmit() {
const formData = new FormData();
// format for request
let txn_names = [];
let txn_prices = [];
this.transactions.forEach(element => {
txn_names.push(element.name);
txn_prices.push(element.price);
});
formData.append("name", this.invoice.name);
formData.append("txn_names", txn_names);
formData.append("txn_prices", txn_prices);
formData.append("user_id", this.$route.params.user.id);
this.loading = "Creating Invoice, please wait ...";
// Post to server
axios.post("http://localhost:3128/invoice", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
this.status = res.data.message;
} else {
this.status = res.data.message;
}
});
}
}
};
</script>

When you go back to the application on localhost:8080 and sign in, you get redirected to a dashboard that looks like this:

View Invoices Component Now that we can create invoices, the next would be to have a visual picture of invoices that have been created and their statuses. To do this, let’s create a ViewInvoices.vue file in the src/components directory of the application.

touch ViewInvoices.vue

Edit the file to look like this:

// src/components/ViewInvoices.vue
<template>
<div>
<div class="container">
<div class="tab-pane fade show active">
<div class="row">
<div class="col-md-12">
<h3>Here are a list of your Invoices</h3>
<table class="table">
<thead>
<tr>
<th scope="col">Invoice #</th>
<th scope="col">Invoice Name</th>
<th scope="col">Status</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<template v-for="invoice in invoices">
<tr>
<th scope="row">{{ invoice.id }}</th>
<td>{{ invoice.name }}</td>
<td v-if="invoice.paid == 0 "> Unpaid </td>
<td v-else> Paid </td>
<td ><a href="#" class="btn btn-success">TO INVOICE</a></td> </tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
[...]

The template above contains a table displaying the invoices a user has created. It also has a button that takes the user to a single invoice view page when an invoice is clicked.

// src/components/ViewInvoice.vue
[...]
<script>
import axios from "axios";
export default {
name: "ViewInvoices",
data() {
return {
invoices: [],
user: this.$route.params.user
};
},
mounted() {
axios
.get(http://localhost:3128/invoice/user/${this.user.id})
.then(res => {
if (res.data.status == true) {
this.invoices = res.data.invoices;
}
});
}
};
</script>

The ViewInvoices component has its data properties as an array of invoices and the user details. The user details are obtained from the route parameters and when the component is mounted, a GET is made to the backend server to fetch the list of invoices created by the user which are then displayed using the template that was shown earlier.

When you go to the Dashboard , click the View Invoices option on the SideNav and you get a view that looks like this:


Conclusion

In this part of the series, we configured the user interface of the invoicing application using concepts from Vue. In the next part of the series, we will take a look at how to add JWT authentication to maintain user sessions, view single invoices and send invoices via email to recipients. Feel free to leave a comment below. Here’s a link to the Github repository.

Thanks for reading

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Learn More

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

Nuxt.js - Vue.js on Steroids

Build Web Apps with Vue JS 2 & Firebase

The Complete Node.js Developer Course (3rd Edition)

Angular & NodeJS - The MEAN Stack Guide

NodeJS - The Complete Guide (incl. MVC, REST APIs, GraphQL)

Node.js: The Complete Guide to Build RESTful APIs (2018)

The JavaScript Developer’s Guide To Node.JS

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

Full Stack Developers: Everything You Need to Know

How to Perform Web-Scraping using Node.js

Google’s Go Essentials For Node.js / JavaScript Developers

Restful API with NodeJS, Express, PostgreSQL, Sequelize, Travis, Mocha, Coveralls and Code Climate

Originally published on https://scotch.io

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

Vue.js is a JavaScript framework with growing number of users. Released 4 years ago, it’s now one of the most populare front-end frameworks. There are some reasons why people like Vue.js. Using Vue.js is very simple if you are already familiar with HTML and JavaScript. They also provide clear documentation and examples, makes it easy for starters to learn the framework. Vue.js can be used for both simple and complex applications. If your application is quite complex, you can use Vuex for state management, which is officially supported. In addition, it’s also very flexible that yu can write template in HTML, JavaScript or JSX.

This tutorial shows you how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB. As for example, we’re going to create a simple application for managing posts which includes list posts, create post, update post and delete post (basic CRUD functionality). I divide this tutorial into two parts. The first part is setting up the Node.js back-end and database. The other part is writing Vue.js code including how to build .vue code using Webpack.

Dependencies

There are some dependencies required for this project. Add the dependencies below to your package.json. Then run npm install to install these dependencies.

  "dependencies": {
    "body-parser": "~1.17.2",
    "dotenv": "~4.0.0",
    "express": "~4.16.3",
    "lodash": "~4.17.10",
    "mongoose": "~5.2.9",
    "morgan": "~1.9.0"
  },
  "devDependencies": {
    "axios": "~0.18.0",
    "babel-core": "~6.26.3",
    "babel-loader": "~7.1.5",
    "babel-preset-env": "~1.7.0",
    "babel-preset-stage-3": "~6.24.1",
    "bootstrap-vue": "~2.0.0-rc.11",
    "cross-env": "~5.2.0",
    "css-loader": "~1.0.0",
    "vue": "~2.5.17",
    "vue-loader": "~15.3.0",
    "vue-router": "~3.0.1",
    "vue-style-loader": "~4.1.2",
    "vue-template-compiler": "~2.5.17",
    "webpack": "~4.16.5",
    "webpack-cli": "^3.1.0"
  },

Project Structure

Below is the overview of directory structure for this project.

  app
    config
    controllers
    models
    queries
    routes
    views
  public
    dist
    src

The app directory contains all files related to server-side. The public directory contains two sub-directories: dist and src. dist is used for the output of build result, while src is for front-end code files.

Model

First, we define a model for Post using Mongoose. To make it simple, it only has two properties: title and content.

app/models/Post.js

  const mongoose = require('mongoose');

  const { Schema } = mongoose;

  const PostSchema = new Schema(
    {
      title: { type: String, trim: true, index: true, default: '' },
      content: { type: String },
    },
    {
      collection: 'posts',
      timestamps: true,
    },
  );

  module.exports = mongoose.model('Post', PostSchema);

Queries

After defining the model, we write some queries that will be needed in the controllers.

app/queries/posts.js

  const Post = require('../models/Post');

  /**
   * Save a post.
   *
   * @param {Object} post - Javascript object or Mongoose object
   * @returns {Promise.}
   */
  exports.save = (post) => {
    if (!(post instanceof Post)) {
      post = new Post(post);
    }

    return post.save();
  };

  /**
   * Get post list.
   * @param {object} [criteria] - Filter options
   * @returns {Promise.<Array.>}
   */
  exports.getPostList = (criteria = {}) => Post.find(criteria);

  /**
   * Get post by ID.
   * @param {string} id - Post ID
   * @returns {Promise.}
   */
  exports.getPostById = id => Post.findOne({ _id: id });

  /**
   * Delete a post.
   * @param {string} id - Post ID
   * @returns {Promise}
   */
  exports.deletePost = id => Post.findByIdAndRemove(id);

Controllers

We need API controllers for handling create post, get post listing, get detail of a post, update a post and delete a post.

app/controllers/api/posts/create.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.save(req.body)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not created'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to create post');
    });

app/controllers/api/posts/delete.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.deletePost(req.params.id)
    .then(() => res.status(200).send())
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to delete post');
    });

app/controllers/api/posts/details.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post');
    });

app/controllers/api/posts/list.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostList(req.params.id)
    .then(posts => res.status(200).send(posts))
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post list');
    });

app/controllers/api/posts/update.js

  const _ = require('lodash');

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then(async (post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      const { title, content } = req.body;

      _.assign(post, {
        title, content
      });

      await postQueries.save(post);

      return res.status(200).send({
        success: true,
        data: post,
      })
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to update post');
    });

Routes

We need to have some pages for user interaction and some API endpoints for processing HTTP requests. To make the app scalable, it’s better to separate the routes for pages and APIs.

app/routes/index.js

  const express = require('express');

  const routes = express.Router();

  routes.use('/api', require('./api'));
  routes.use('/', require('./pages'));

  module.exports = routes;


Below is the API routes.

app/routes/api/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', require('../../controllers/api/posts/list'));
  router.get('/posts/:id', require('../../controllers/api/posts/details'));
  router.post('/posts/', require('../../controllers/api/posts/create'));
  router.patch('/posts/:id', require('../../controllers/api/posts/update'));
  router.delete('/posts/:id', require('../../controllers/api/posts/delete'));

  module.exports = router;


For the pages, in this tutorial, we use plain HTML file. You can easily replace it with any HTML template engine if you want. The HTML file contains a div whose id is app. Later, in Vue.js application, it will use the element with id app for rendering the content. What will be rendered on each pages is configured on Vue.js route on part 2 of this tutorial.

app/routes/pages/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/create', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/:id', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  module.exports = router;

Below is the HTML file

app/views/index.html

  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>VueJS Tutorial by Woolha.com</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" type="text/css" media="all" />
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
      <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    </head>
    <body>
      <div id="app"></div>
      <script src="/dist/js/main.js"></script>
    </body>
  </html>

Below is the main script of the application, you need to run this for starting the server-side application.

app/index.js

  require('dotenv').config();

  const bodyParser = require('body-parser');
  const express = require('express');
  const http = require('http');
  const mongoose = require('mongoose');
  const morgan = require('morgan');
  const path = require('path');

  const dbConfig = require('./config/database');
  const routes = require('./routes');

  const app = express();
  const port = process.env.PORT || 4000;

  global.__basedir = __dirname;

  mongoose.Promise = global.Promise;

  mongoose.connect(dbConfig.url, dbConfig.options, (err) => {
    if (err) {
      console.error(err.stack || err);
    }
  });

  /* General setup */
  app.use(morgan('dev'));
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(morgan('dev'));

  app.use('/', routes);

  const MAX_AGE = 86400000;

  // Select which directories or files under public can be served to users
  app.use('/', express.static(path.join(__dirname, '../public'), { maxAge: MAX_AGE }));

  // Error handler
  app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
    res.status(err.status || 500);

    if (err.status === 404) {
      res.locals.page = {
        title: 'Not Found',
        noIndex: true,
      };

      console.error(`Not found: ${req.url}`);

      return res.status(404).send();
    }

    console.error(err.stack || err);

    return res.status(500).send();
  });

  http
    .createServer(app)
    .listen(port, () => {
      console.info(`HTTP server started on port ${port}`);
    })
    .on('error', (err) => {
      console.error(err.stack || err);
    });

  process.on('uncaughtException', (err) => {
    if (err.name === 'MongoError') {
      mongoose.connection.emit('error', err);
    } else {
      console.error(err.stack || err);
    }
  });

  module.exports = app;

That’s all for the server side preparation. On the next part, we’re going to set up the Vue.js client-side application and build the code into a single JavaScript file ready to be loaded from HTML.

Then, we build the code using Webpack, so that it can be loaded from HTML. In this tutorial, we’re building a simple application with basic CRUD functionality for managing posts.

Create Vue.js Components

For managing posts, there are three components we’re going to create. The first one is for creating a new post. The second is for editing a post. The other is for managing posts (displaying list of posts and allow post deletion)

First, this is the component for creating a new post. It has one method createPost which validate data and send HTTP request to the server. We use axios for sending HTTP request.

public/src/components/Posts/Create.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Create a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="createPost">Create Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          title: '',
          content: '',
        },
      }),
      methods: {
        createPost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = 'http://localhost:4000/api/posts';
          const param = this.post;

          axios
            .post(url, param)
            .then((response) => {
              console.log(response);
              window.location.href = 'http://localhost:4000/posts';
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


Below is the component for editing a post. Of course, we need the current data of the post before editing it. Therefore, there’s fetchPost method called when the component is created. There’s also updatePost method which validate data and call the API for updating post.

public/src/components/Posts/Edit.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Edit a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="updatePost">Update Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          _id: '',
          title: '',
          content: '',
        },
      }),
      created: function() {
        this.fetchPost();
      },
      methods: {
        fetchPost() {
          const postId = this.$route.params.id;
          const url = `http://localhost:4000/api/posts/${postId}`;

          axios
            .get(url)
            .then((response) => {
              this.post = response.data;
              console.log('this.post;');
              console.log(this.post);
          });
        },
        updatePost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = `http://localhost:4000/api/posts/${this.post._id}`;
          const param = this.post;

          axios
            .patch(url, param)
            .then((response) => {
                console.log(response);
              window.alert('Post successfully saved');
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


For managing posts, we need to fetch the list of post first. Similar to the edit component, in this component, we have fetchPosts method called when the component is created. For deleting a post, there’s also a method deletePost. If post successfully deleted, the fetchPosts method is called again to refresh the post list.

public/src/components/Posts/List.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Post List</h1>
      <b-button variant="primary" style="color: #ffffff; margin: 20px;"><a href="/posts/create" style="color: #ffffff;">Create New Post</a></b-button>
      <b-container-fluid v-if="posts.length">
        <table class="table">
          <thead>
            <tr class="d-flex">
              <td class="col-8">Titleqqqqqqqqq</td>
              <td class="col-4">Actions</td>
            </tr>
          </thead>
          <tbody>
            <tr v-for="post in posts" class="d-flex">
              <td class="col-8">{{ post.title }}</td>
              <td class="col-2"><a v-bind:href="'http://localhost:4000/posts/' + post._id"><button type="button" class="btn btn-primary"><i class="fa fa-edit" aria-hidden="true"></i></button></a></td>
              <td class="col-2"><button type="button" class="btn btn-danger" v-on:click="deletePost(post._id)"><i class="fa fa-remove" aria-hidden="true"></i></button></td>
            </tr>
          </tbody>
        </table>
      </b-container-fluid>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        posts: [],
      }),
      created: () => {
        this.fetchPosts();
      },
      methods: {
        fetchPosts() {
          const url = 'http://localhost:4000/api/posts/';

          axios
            .get(url)
            .then((response) => {
              console.log(response.data);
              this.posts = response.data;
          });
        },
        deletePost(id) {
          if (event) {
            event.preventDefault();
          }

          const url = `http://localhost:4000/api/posts/${id}`;
          const param = this.post;

          axios
            .delete(url, param)
            .then((response) => {
              console.log(response);
              console.log('Post successfully deleted');

              this.fetchPosts();
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


All of the components above are wrapped into a root component which roles as the basic template. The root component renders the navbar which is same across all components. The component for each routes will be rendered on router-view.

public/src/App.vue

  <template>
    <div>
      <b-navbar toggleable="md" type="dark" variant="dark">
        <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
        <b-navbar-brand to="/">My Vue App</b-navbar-brand>
        <b-collapse is-nav id="nav_collapse">
          <b-navbar-nav>
            <b-nav-item to="/">Home</b-nav-item>
            <b-nav-item to="/posts">Manage Posts</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
      <!-- routes will be rendered here -->
      <router-view />
    </div>
  </template>

  <script>

  export default {
    name: 'app',
    data () {},
    methods: {}
  }
  </script>


For determining which component should be rendered, we use Vue.js’ router. For each routes, we need to define the path, component name and the component itself. A component will be rendered if the current URL matches the path.

public/src/router/index.js

  import Vue from 'vue'
  import Router from 'vue-router'

  import CreatePost from '../components/Posts/Create.vue';
  import EditPost from '../components/Posts/Edit.vue';
  import ListPost from '../components/Posts/List.vue';

  Vue.use(Router);

  let router = new Router({
    mode: 'history',
    routes: [
      {
        path: '/posts',
        name: 'ListPost',
        component: ListPost,
      },
      {
        path: '/posts/create',
        name: 'CreatePost',
        component: CreatePost,
      },
      {
        path: '/posts/:id',
        name: 'EditPost',
        component: EditPost,
      },
    ]
  });

  export default router;


Lastly, we need a main script as the entry point which imports the main App component and the router. Inside, it creates a new Vue instance

webpack.config.js

  import BootstrapVue from 'bootstrap-vue';
  import Vue from 'vue';

  import App from './App.vue';
  import router from './router';

  Vue.use(BootstrapVue);
  Vue.config.productionTip = false;
  new Vue({
    el: '#app',
    router,
    render: h => h(App),
  });

Configure Webpack

For building the code into a single JavaSript file. Below is the basic configuration for Webpack 4.

webpack.config.js

  const { VueLoaderPlugin } = require('vue-loader');

  module.exports = {
    entry: './public/src/main.js',
    output: {
      path: `${__dirname}/public/dist/js/`,
      filename: '[name].js',
    },
    resolve: {
      modules: [
        'node_modules',
      ],
      alias: {
        // vue: './vue.js'
      }
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'vue-style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.vue$/,
          loader: 'vue-loader',
          options: {
            loaders: {
            }
            // other vue-loader options go here
          }
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          exclude: /node_modules/
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin(),
    ]

After that, run ./node_modules/webpack/bin/webpack.js. You can add the command to the scripts section of package.json, so you can run Webpack with a shorter command npm run build, as examplified below.

  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  "scripts": {
    "build": "./node_modules/webpack/bin/webpack.js",
    "start": "node app/index.js"
  },

Finally, you can start to try the application. This code is also available on Woolha.com’s Github.

How to implement server-side pagination in Vue.js with Node.js

How to implement server-side pagination in Vue.js with Node.js

This is a simple example of how to implement server-side pagination in Vue.js with a Node.js backend API.

Originally published at https://jasonwatmore.com
This is a simple example of how to implement pagination in Vue.js with a Node.js backend API.

The example contains a hard coded array of 150 objects split into 30 pages (5 items per page) to demonstrate how the pagination logic works. Styling of the example is done with Bootstap 4.

The tutorial code is available on GitHub at https://github.com/cornflourblue/vue-node-server-side-pagination.

Here it is in action (may take a few seconds for the container to startup):

(See on CodeSandbox at https://codesandbox.io/s/vuejs-node-server-side-pagination-example-0l40x)

Running the Vue.js + Node Pagination Example Locally
  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the tutorial project source code from https://github.com/cornflourblue/vue-node-server-side-pagination.
  3. Install required npm packages of the backend Node API by running the npm install command in the /server folder.
  4. Start the backend Node API by running npm start in the /server folder,
  5. this will start the API on the URL http://localhost:4000.
  6. Install required npm packages of the frontend Vue.js app by running the npm install command in the /client folder.
  7. Start the Vue.js frontend app by running npm start in the /client folder,
  8. this will build the app with webpack and automatically launch it in a
  9. browser on the URL http://localhost:8080.
Server-Side (Node.js) Pagination Logic

Pagination is handled by the backend Node API with the help of the jw-paginate npm package, for more info on how the pagination logic works see JavaScript - Pure Pagination Logic in Vanilla JS / TypeScript.

Below is the code for the paged items route (/api/items) in the node server file (/server/server.js) in the example, it creates a hardcoded list of 150 items to be paged, in a real application you would replace this with real data (e.g. from a database). The route accepts an optional page parameter in the url query string, if the parameter isn't set it defaults to the first page.

The paginate() function is from the jw-paginate package and accepts the following parameters:

  • totalItems (required) - the total number of items to be paged
  • currentPage (optional) - the current active page, defaults to the first page
  • pageSize (optional) - the number of items per page, defaults to 10
  • maxPages (optional) - the maximum number of page navigation links to display, defaults to 10

The output of the paginate function is a pager object containing all the information needed to get the current pageOfItems out of the items array, and to display the pagination controls in the Vue.js frontend, including:

  • startIndex - the index of the first item of the current page (e.g. 0)
  • endIndex - the index of the last item of the current page (e.g. 9)
  • pages - the array of page numbers to display (e.g. [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ])
  • currentPage - the current active page (e.g. 1)
  • totalPages - the total number of pages (e.g. 30)

I've set the pageSize to 5 in the CodeSandbox example above so the pagination links aren't hidden below the terminal console when the container starts up. In the code on GitHub I didn't set the page size so the default 10 items are displayed per page in that version.

The current pageOfItems is extracted from the items array using the startIndex and endIndex from the pager object. The route then returns the pager object and current page of items in a JSON response.

// paged items route
app.get('/api/items', (req, res, next) => {
   // example array of 150 items to be paged
   const items = [...Array(150).keys()].map(i => ({ id: (i + 1), name: 'Item ' + (i + 1) }));

   // get page from query params or default to first page
   const page = parseInt(req.query.page) || 1;

   // get pager object for specified page
   const pageSize = 5;
   const pager = paginate(items.length, page, pageSize);

   // get page of items from items array
   const pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);

   // return pager object and current page of items
   return res.json({ pager, pageOfItems });
});

Client-Side (Vue.js) Pagination Component

Since the pagination logic is handled on the server, the only thing the Vue.js client needs to do is fetch the pager information and current page of items from the backend, and display them to the user.

Vue.js Home Page Component

Below is the Vue home page component (/client/src/home/HomePage.vue) from the example. The template renders the current page of items as a list of divs with the v-for directive, and renders the pagination controls using the data from the pager object. Each pagination link sets the page query parameter in the url with the <router-link> component and :to="{ query: { page: ... }}" property.

The Vue component contains a watcher function on the page url query parameter '$route.query.page', the handler function is triggered by Vue whenever the page variable in the url querystring changes, the immediate: true flag tells Vue to also run the function when the component first loads. The watcher function checks if the page has changed and fetches the pager object and pageOfItems for the current page from the backend API with an HTTP request.

The CSS classes used are all part of Bootstrap 4.3, for more info see https://getbootstrap.com/docs/4.3/getting-started/introduction/.

<template>
   <div class="card text-center m-3">
       <h3 class="card-header">Vue.js + Node - Server Side Pagination Example</h3>
       <div class="card-body">
           <div v-for="item in pageOfItems" :key="item.id">{{item.name}}</div>
       </div>
       <div class="card-footer pb-0 pt-3">
           <ul v-if="pager.pages && pager.pages.length" class="pagination">
               <li :class="{'disabled':pager.currentPage === 1}" class="page-item first-item">
                   <router-link :to="{ query: { page: 1 }}" class="page-link">First</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === 1}" class="page-item previous-item">
                   <router-link :to="{ query: { page: pager.currentPage - 1 }}" class="page-link">Previous</router-link>
               </li>
               <li v-for="page in pager.pages" :key="page" :class="{'active':pager.currentPage === page}" class="page-item number-item">
                   <router-link :to="{ query: { page: page }}" class="page-link">{{page}}</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === pager.totalPages}" class="page-item next-item">
                   <router-link :to="{ query: { page: pager.currentPage + 1 }}" class="page-link">Next</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === pager.totalPages}" class="page-item last-item">
                   <router-link :to="{ query: { page: pager.totalPages }}" class="page-link">Last</router-link>
               </li>
           </ul>
       </div>
   </div>
</template>

<script>
export default {
   data () {
       return {
           pager: {},
           pageOfItems: []
       }
   },
   watch: {
       '$route.query.page': {
           immediate: true,
           handler(page) {
               page = parseInt(page) || 1;
               if (page !== this.pager.currentPage) {
                   fetch(`/api/items?page=${page}`, { method: 'GET' })
                       .then(response => response.json())
                       .then(({pager, pageOfItems}) => {
                           this.pager = pager;
                           this.pageOfItems = pageOfItems;
                       });
               }
           }
       }
   }
}
</script>

How to Creat a Task Management App Using Vue.js and a Node.js

How to Creat a Task Management App Using Vue.js and a Node.js

In this article, we will build a simple Task Manager application. We will build it using Node.js in the backend and Vue.js in the frontend. We will have a list of boards, each containing several lists. Each list will contain several cards which will represent the tasks.

Node.js is a Javascript backend framework and has been around for many years. In comparison to more established frameworks for Java, PHP, and Ruby, it is still young. Even with this, there is still some positive news. There are many JavaScript backend frameworks in the top starred projects on GitHub.

Javascript's flexibility allows it to work on both the frontend and backend. There are several components and patterns which seem to be common among many applications: drag-and-drop widgets, rearranging items to change their order, seeding data in the backend for development purposes, editable-inline-fields, a central event system for sending messages across different parts of an application, among others. There are several frontend libraries to help with the above. We will not be using any of them. We will build the above-mentioned features from scratch. This is so that we are aware of how the underlying mechanics of the base web technologies work.

In this article, we will build a simple Task Manager application. We will build it using Node.js in the backend and Vue.js in the frontend. We will have a list of boards, each containing several lists. Each list will contain several cards which will represent the tasks.

Being acquainted with the basic structure of the application, let's start building it. We will need Node.js installed to follow along.

The finished code for this tutorial is accessible at these GitHub repositories:

Initialise the Backend and Frontend Application Folders

In an empty folder, initialize the frontend using

vue init webpack client 

Accept with yes for all the prompts, except for “tests”. Enter the required information where necessary. Also, accept to install the dependencies using npm.

Navigate to the frontend folder using cd client. Install the required packages using;

npm install --save axios

This is for making HTTP calls to the server.

Install the following packages which we will use for compiling Sass:

npm install sass-loader node-sass --save-dev

Navigate to the base folder using cd ../ and create the server folder called server. Navigate to the folder using cd server and create a file named index.js. This will be the entry point for the server. Initialize a new package.json file using npm init. Answer yes to all the prompts or fill in the details if you deem it necessary.

Install the following server packages;

npm install --save body-parser express faker mongoose

The packages we installed are responsible for:

  • body-parser: this helps us to parse and use frontend parameters in the backend.
  • express: A backend framework for Node.js.
  • faker: A library to help create dummy data for seeding database during development.
  • Mongoose: A database ORM library to do operations with the MongoDB database.

In the file index.js, paste in the following:

const mongoose = require('mongoose')
const express = require('express')
const app = express()
const port = 3000
const bodyParser = require('body-parser')
const config = require('./config/index')
const seederService = require('./services/seeder.service');

mongoose.connect(config.dbConnection, { useNewUrlParser: true})

app.use(bodyParser.json())

const corsConfig = function(req, res, next) {
    res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
  res.header('Access-Control-Allow-Credentials', true)
  res.header('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT')
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
  next()
}

app.use(corsConfig);

const apiRoutes = require('./routes/api');
app.use('/api', apiRoutes);

if (config.seedData) {
  seederService.seedData()
}

app.listen(port, () => console.log(Example app listening on port ${port}!))

In the above code, we are initializing the Express.js server. We instruct the server to allow frontend connections from certain URLs — in this case: http://localhost:8080. Next, we import the seeder and config file ./services/seeder.service and ./config/index. These do not exist yet but we will get to that soon. We will only initialize the seeding mechanism if the config option seedData is set to true. This should only for use during development but that choice is up to the developer.

Create the config file in config/index.js and paste in the following:

module.exports = {
  seedData: true,
  dbConnection: "mongodb://127.0.0.1:27017/task-management-system",
  numberOfBoards: 15,
  numberOfListsPerBoard: 8,
  numberOfCardsPerList: 4
}

We will need MongoDB running to carry on with this tutorial. For more information on that, please follow the instructions in the article How to Create a Public File Sharing Service with Vue.js and Node.js. This was published recently.

Create Backend Data Structure For Cards, Lists and Boards

Cards Data Structure

Now, let us create the models for the entities of our application. Still in the server folder; create the board model in the file models/board.model.js and paste in the following:

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const Types = Schema.Types;
const List = require("./list.model")

const BoardSchema = new Schema({
  title: Types.String,
  lists: [
    {type: Types.ObjectId, ref: "List", default: []}
  ]
})

module.exports = mongoose.model("Board", BoardSchema, "boards")

Create the list model in the file models/list.model.js and paste in the following:

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const Types = Schema.Types;
const Card = require("./card.model")

const ListSchema = new Schema({
  title: Types.String,
  cards: [
    {type: Types.ObjectId, ref: "Card", default: []}
  ]
})

module.exports = mongoose.model("List", ListSchema, "lists")

Create the card model in the file models/card.model.js and paste in the following:

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const Types = Schema.Types;

const CardSchema = new Schema({
  title: Types.String,
  body: Types.String,
})

module.exports = mongoose.model("Card", CardSchema, "cards")

Seed Some Data into the Boards, Cards and Lists Database (MongoDB)

Let us now create the seeder service. This will create some initial data in the database for us to use during development. Create a file in services/seeder.service.js and paste in the following:

const faker = require('faker')
const Board = require('../models/board.model')
const List = require('../models/list.model')
const Card = require('../models/card.model')
const config = require('../config/index')

module.exports = {
  seedData () {
    Board.countDocuments((err, count) => {
      if (count > 0) {
        return;
      }

      this.createBoards()
    })
  },
  createBoards () {
    let boards = [];

    Array.from(Array(config.numberOfBoards)).forEach(() => {
      boards.push({
        title: faker.lorem.sentence(7)
      })
    })

    Board.insertMany(boards, (err, savedBoards) => {
      this.createListsForBoards(savedBoards)
    })
  },
  createListsForBoards (boards) {
    boards.forEach((board) => {
      this.createLists(board)
    })
  },
  createLists (board) {
    let lists = [];
    Array.from(Array(config.numberOfListsPerBoard)).forEach((val, index) => {
      lists.push({
        title: index + faker.lorem.sentence(3),
      })
    })

    List.insertMany(lists, (err, savedLists) => {
      savedLists.forEach((savedList) => {   
        board.lists.push(savedList.id)
      })
      board.save(() => {
        this.createCardsForLists(savedLists)
      })
    })
  },
  createCardsForLists (lists) {
    lists.forEach((list) => {
      this.createCards(list)
    })
  },
  createCards (list) {
    let cards = [];

    Array.from(Array(config.numberOfCardsPerList)).forEach(() => {
      cards.push({
        title: faker.lorem.sentence(5),
        body: faker.lorem.paragraph(1),
      })
    })

    Card.insertMany(cards, (err, savedCards) => {
      savedCards.forEach((savedCard) => {
        list.cards.push(savedCard.id)
      })
      list.save()
    })
  }
}

The method seedData initializes the seeder; we are creating several boards using createBoards. We will create several lists for each of these boards using createListsForBoards. Then, we are creating several cards for each of the lists using createCardsForLists.

Display the Boards, Cards and Lists

Backend

Now that we have the basic infrastructure in place, let's build the routes. These will be responsible for communicating with the frontend.

Create a route file in routes/api.js. Paste in the following:

const express = require("express")
const router = express.Router()
const boardService = require("../services/board.service")

router.get("/boards", boardService.getAll.bind(boardService))
router.get("/boards/:boardId", boardService.getById.bind(boardService))

module.exports = router

This declares the routes to list all the boards. We also have a route to get a single board along with all the nested lists and cards.

Next, create the board-service in services/board.service.js and paste in the following:

const Board = require('../models/board.model')

module.exports = {
  getAll (req, res) {
    Board.find({}, 'title', (err, boards) => {
      this._handleResponse(err, boards, res)
    })
  },
  getById (req, res) {
    Board.findOne({_id: req.params.boardId})
      .populate({
        path: "lists",
        select: ["title"],
        model: "List",
        populate: {
          path: "cards",
          select: ["title", "body"],
          model: "Card"
        }
      })
      .exec((err, board) => {
        this._handleResponse(err, board, res)
      })
  },
  _handleResponse (err, data, res) {
    if (err) {
      res.status(400).end()
    } else {
      res.send(data)
    }
  }
}

This service will handle the requests for the routes above.

Frontend

Now, let's create the frontend files for listing the boards, lists and cards. Navigate to the frontend folder client. In the file src/App.vue, remove the piece of code-line:

<img src="./assets/logo.png">

In the style section, replace it with the following:

<style>
html {
 box-sizing: border-box;
 font-size: 62.5%;
}
body {
 margin: 0;
}
html, body, #app {
 height: 100%;
}
body {
 background-color: #4fc08d;
 font-size: 1.6rem;
 font-family: Helvetica Neue,Arial,Helvetica,sans-serif;
 line-height: 20px;
}
*, *:before, *:after {
 box-sizing: inherit;
}
</style>

This is doing some minimal resets and base styles.

In the file src/main.js, after the line Vue.config.productionTip = false, add the following:

axios.defaults.baseURL = 'http://localhost:3000'

Import the axios library using:

import axios from 'axios'

This configures the axios library to direct all HTTP calls to the URL http://localhost:3000.

To list the boards, open the route file src/router/index.js. Change the existing routes to the following:

import Vue from "vue"
import Router from "vue-router"
import Boards from "@/components/Boards"
import BoardPage from "@/components/BoardPage"

Vue.use(Router)

export default new Router({
 routes: [
  {
   path: "/",
   name: "Boards",
   component: Boards
  },
  {
   path: "/boards/:boardId",
   name: "BoardPage",
   component: BoardPage
  }
 ]
})

We have some pending components to create. Let’s do that now:

First, let’s create the boards component in src/components/Boards.vue. Paste in the following:

<template>
 <div class="boards">
  <router-link v-for="board in boards" class="board" :key="board._id" :to="{ name: 'BoardPage', params: {boardId: board._id }}">
   {{ board.title }}
  </router-link>
 </div>
</template>

<script>
import boardService from "../services/board.service"
export default {
 name: "Boards",
 data () {
  return {
   boards: []
  }
 },
 mounted () {
  boardService.getAll()
   .then(((boards) => {
    this.$set(this, "boards", boards)
   }).bind(this))
 }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.boards {
 width: 80%;
 margin: 0 auto;
 padding-top: 100px;
 display: flex;
 flex-wrap: wrap;
}
.board {
 border-radius: 3px;
 color: #FFFFFF;
 display: block;
 text-decoration: none;
 width: 15%;
 min-width: 150px;
 min-height: 80px;
 padding: 10px;
 background-color: rgb(0, 121, 191);
 margin: 0 15px 15px 0;
}
</style>

The code above uses the board service to fetch the boards from the backend and loops over them to display. We have some CSS styles as well.

Next, create the frontend board service in src/services/board.service.js. Paste in the following:

import axios from "axios"

export default {
 getAll() {
  return axios.get("/api/boards").then(res => res.data)
 },
 findById(boardId) {
  return axios.get("/api/boards/" + boardId).then(res => res.data)
 }
}

Now, create the single-board component in src/components/BoardPage.vue. Put in the following:

<template>
 <div class="board-page-main">
  <template v-if="board">
   <div class="board-title">
    <h2>{{ board.title }}</h2>
   </div>
   <div class="board-lists">
    <div class="board-lists-inner">
     <list 
      v-for="(list, i) in lists" 
      :key="list._id" 
      :index="i" 
      :list-prop="list"/>
    </div>
   </div>
  </template>
 </div>
</template>

<script>
import boardService from "../services/board.service";
import List from "./List";
export default {
 components: {

 },
 data() {
  return {
   board: null,
   lists: []
  };
 },
 created() {
   
 },
 mounted() {
  boardService.findById(this.$route.params.boardId).then(
   (board => {
    this.$set(this, "board", board);
    this.$set(this, "lists", board.lists);
   }).bind(this)
  );
 },
 methods: {
   
 },
};
</script>

<style>
.board-title .is-editing {
 background-color: #ffffff;
 color: #000000;
 padding: 8px;
 display: inline-block;
 min-width: 600px;
}
.add-new-list .is-editing {
 background-color: #ffffff;
 color: #000000;
 padding: 8px;
 margin: 0;
}
</style>

<style scoped lang="scss">
.add-new-list {
 display: inline-block;
 width: 270px;
}
.board-title {
 color: #ffffff;
 padding: 10px;
 height: 90px;
}
.board-page-main {
 height: 100%;
 display: flex;
 flex-direction: column;
}
.board-lists {
 flex-grow: 1;
 margin-bottom: 20px;
 position: relative;
}
.board-lists-inner {
 top: 0;
 right: 0;
 bottom: 0;
 left: 0;
 overflow-x: scroll;
 position: absolute;
 white-space: nowrap;
}
</style>

Then, create the single-list component in src/components/List.vue . Add in the following:

<template>
 <div
  class="board-list"
 >
  <div class="list-inner">
   <div 
    v-if="list" 
    class="list-title">
    <h3>
     {{ list.title }}
    </h3>
   </div>
   <div class="list-cards">
    <card
     v-for="(card, i) in cards"
     :card-prop="card"
     :list-prop="list"
     :key="card._id"
     :index="i"
    />
   </div>
  </div>
 </div>
</template>

<script>
import Card from "./Card"
export default {
 components: {
  Card
 },
 props: [
  "listProp",
  "index"
 ],
 data () {
  return {
   list: null,
   cards: []
  }
 },
 mounted () {
  this.$set(this, "list", this.listProp)
  this.$set(this, "cards", this.listProp.cards)
 },
 methods: {
   
 },
}
</script>

<style>
.list-title .is-editing {
 background-color: #ffffff;
 color: #000000;
}
</style>

<style scoped lang="scss">
.list-inner {
 background-color: #dfe3e6;
 padding: 10px;
 white-space: normal;
 border-radius: 3px;
}
.board-list {
 display: inline-block;
 margin-bottom: 10px;
 margin-right: 10px;
 vertical-align: top;
 width: 270px;
 max-height: 100%;
 overflow-y: scroll;
 &.is-dragging-list {
  transform: rotate(1deg)
 }
 &.drag-entered {
  border: 3px solid #237bda;
 }
}
</style>

For the final component, create the card-component in src/components/Card.vue. Add in the following:

<template>
 <div
  v-if="card"
  class="list-card"
 >
  <div class="card-title">{{ card.title }}</div>
  <div class="card-body">{{ card.body }}</div>
 </div>
</template>

<script>
export default {
 props: ["cardProp", "listProp", "index"],
 data() {
  return {
   card: null,
   list: null
  };
 },
 mounted() {
  this.$set(this, "card", this.cardProp);
  this.$set(this, "list", this.listProp);
 },
 methods: {
   
 },
};
</script>

<style scoped lang="scss">
.card-title {
 text-decoration: underline;
}
.list-card {
 background-color: #ffffff;
 border-radius: 3px;
 padding: 10px;
 margin-bottom: 10px;

 &.is-dragging-card {
  transform: rotate(1deg);
 }
 &.drag-entered {
  border: 3px solid #237bda;
 }
}
</style>

Edit Board Title

Backend

Let's now set up the backend for editing a board title. Navigate to the backend folder. In the file services/board.service.js, add the following method:

update (req, res) {
  Board.findByIdAndUpdate(req.params.boardId, {title: req.body.title}, (err, board) => {
    this._handleResponse(err, board, res)
  })
}

Inside the router routes/api.js, add the following:

router.put("/boards/:boardId", boardService.update.bind(boardService))

Frontend

Now, navigate to the frontend folder. In the component file src/components/BoardPage.vue, locate the code snippet below:

<h2>{{ board.title }}</h2>

And replace it with this one:

<editable
 v-slot:default="slotProps"
 :field-value="board.title"
 @editable-submit="editableSubmitted"
 >
 <h2>{{ slotProps.inputText }}</h2>
</editable>

In the above code, we are invoking a component named Editable, but we are yet to create that. Before we do that, let's explain how it will behave. The component will be a general component responsible for editing text. We expect it to emit an event named editable-submit anytime the user modifies the value. We also pass in a property to act as the initial value to display in the component using :field-value="board.title". Since the component will have a slot section, we passed in the HTML to display in there. Then, we will get access to the component's data scope using v-slot:default="slotProps".

Add this method to src/components/BoardPage.vue to handle the emitted event above:

editableSubmitted(inputText) {
 if (inputText === this.board.title) {
  return;
 }
 boardService.update(this.board._id, inputText).then(() => {
  this.board.title = inputText;
 })
},

Import the Editable component using the line:

import Editable from "./Editable";

And add it to the components list as shown below:

components: {
 List,
 Editable
},

Now, let's create the Editable component. Create a file in src/components/Editable.vue. Paste in the following:

<template>
 <div>
  <h2
   v-show="isEditing"
   ref="editableField"
   :class="{'is-editing': isEditing}"
   contenteditable="true"
   @keydown.enter="submit"
   @blur="onBlur"
   @keydown.esc="escape"
  >{{ inputText }}</h2>
  <template v-if="isEditing === false">
   <div @click="onBoardTitleClick()">
    <slot 
     :isEditing="isEditing" 
     :inputText="inputText"/>
   </div>
  </template>
 </div>
</template>

<script>
export default {
 props: ["fieldValue"],
 data() {
  return {
   inputText: "",
   isEditing: false
  };
 },
 mounted() {
  this.$set(this, "inputText", this.fieldValue);
 },
 methods: {
  onBoardTitleClick() {
   this.$set(this, "isEditing", true);
   setTimeout((() => {
    this.$refs.editableField.focus()
   }).bind(this), 200)
  },
  submit(event) {
   this.$set(this, "inputText", event.currentTarget.innerText)
   this.$emit("editable-submit", event.currentTarget.innerText)
   this.$set(this, "isEditing", false);
  },
  escape(event) {
   this.$set(this, "inputText", event.currentTarget.innerText)
   this.$emit("editable-submit", event.currentTarget.innerText);
   this.$set(this, "isEditing", false);
  },
  onBlur (event) {
   this.$set(this, "inputText", event.currentTarget.innerText)
   this.$emit("editable-submit", event.currentTarget.innerText);
   this.$set(this, "isEditing", false);
  }
 }
};
</script>

In this component, we have a slot section which shows when we are not in edit mode. When in edit mode, the content-editable div is displayed. We are listening to several events. When the title gets clicked, we set the component in edit mode. This will display the content editable div. When in this mode, we can change the title. When we press the escape key, an event called editable-submit is emitted with the updated value to the parent. Likewise, we do the same for when the user presses the enter key. When blurred, we disable edit-mode but keep the changed value if any.

The parent component src/components/BoardPage.vue listens for the editable-submit event. We have not defined the handler yet. Let's do that now. Inside the parent, add the method below:

editableSubmitted(inputText) {
 if (inputText === this.board.title) {
  return;
 }
 boardService.update(this.board._id, inputText).then(() => {
  this.board.title = inputText;
 })
},

This calls the board-service to update the board on the backend. If successful, we set the board title with the value emitted from the Editable component.

In the board-service src/services/board.service.js, add the update method below:

update(boardId, title) {
 return axios.put(
   "/api/boards/" + boardId,
   {
    title: title
   }
  ).then(res => res.data)
}
Add a New List

Backend

Now, let's set up the backend infrastructure to be able to add a new list. Navigate to the backend folder. Create a file in services/list.service.js and add the following:

const List = require('../models/list.model')
const Board = require('../models/board.model')

module.exports = {
  create (req, res) {
    Board.findById(req.body.boardId, (err, board) => {
      if (err) {
        return this._handleResponse(err, null, res)
      }

      if (!board) {
        return this._handleResponse("Error", null, res)
      }

      List.create({title: req.body.title}, (err, card) => {
        board.lists.push(card._id)
        board.save(() => {
          this._handleResponse(err, card, res)
        })
      })
    })
  },
  _handleResponse (err, data, res) {
    if (err) {
      res.status(400).end()
    } else {
      res.send(data)
    }
  }
}

Next, in the route file routes/api.js, add the following:

router.post("/lists", listService.create.bind(listService))

And also import the list-service like below:

const listService = require("../services/list.service")

Frontend

Navigate to the frontend folder. In the file src/components/BoardPage.vue, at the end of the tag:

<div class="board-lists-inner">

Add in the following:

<addable
 class="add-new-list"
 @addable-submit="addableSubmit">
 <div>Add list</div>
</addable>

We introduced another general component called Addable. This is will be responsible for adding new items. It will have a slot inside it as well. We expect it to emit an event called addable-submit.

Let's add a method to handle this event. Inside the file src/components/BoardPage.vue, add the method below:

addableSubmit(listTitle) {
 if (!listTitle || listTitle.length === 0) {
  return;
 }
 listService.create(this.board._id, listTitle).then((newList) => {
  this.board.lists.push(newList)
 })
},

Import the Addable component and the list-service with the following:

import listService from "../services/list.service";
import Addable from "./Addable";

Next, add the Addable component to the list of imported components like below:

components: {
 List,
 Editable,
 Addable,
}

Create a list-service inside src/services/list.service.js. Put in the following:

import axios from "axios"

export default {
 create (boardId, listTitle) {
  return axios.post("/api/lists", {
   boardId: boardId,
   title: listTitle
  }).then(res => res.data)
 }
}

Finally, let's create the component itself. Create a file in src/components/Addable.vue. Paste the following in:

<template>
 <div>
  <h2
   v-show="isAdding"
   ref="addableField"
   :class="{'is-editing': isAdding}"
   contenteditable="true"
   @keydown.enter="submit"
   @blur="onBlur"
   @keydown.esc="escape"
  >{{ inputText }}</h2>
  <h2>{{ inputText }}</h2>
  <template v-if="isAdding === false">
   <div @click="onTitleClick()">
    <slot/>
   </div>
  </template>
 </div>
</template>

<script>
export default {
 data () {
  return {
   isAdding: false,
   inputText: ""
  }
 },
 methods: {
  onTitleClick () {
   this.$set(this, "isAdding", true)
   setTimeout((() => {
    this.$refs.addableField.focus()
   }).bind(this), 200)
  },
  onBlur () {
   this.$set(this, "isAdding", false)
  },
  escape () {
   this.emptyInput()
  },
  submit (event) {
   this.$emit("addable-submit", event.currentTarget.innerText)
   this.emptyInput()
  },
  emptyInput () {
   this.$set(this, "inputText", "")
   this.$refs.addableField.innerText = ""
   this.$set(this, "isAdding", false);
  }
 }
}
</script>

In here, when the text value is clicked, we set the component to add mode. We are showing the slot template when not in add mode. When in add mode, we show the editable div. When the user presses enter, we empty the input and emit an event named addable-submit. When escape is pressed, we empty the input and disable add mode but no event is emitted. When the editable div is blurred, we disable add mode.

Rearrange List

Backend

For our last feature, let's set up the backend feature to rearrange lists. Navigate to the backend folder. Inside routes/api.js, add the following route:

router.put(
 "/boards/updateListsOrder",
 boardService.updateListsOrder.bind(boardService)
)

Rearrange the routes so that the following two are in this order. If not you might have conflicting issues:

router.put(
  "/boards/updateListsOrder",
  boardService.updateListsOrder.bind(boardService)
 )
router.put("/boards/:boardId", boardService.update.bind(boardService))

Inside services/board.service.js, add the method below:

updateListsOrder (req, res) {
  Board.findById(req.body.boardId, (err, board) => {
    if (err) {
      res.status(400).end()
      return
    }

    board.lists = req.body.listIds
    board.save((err, savedBoard) => {
      this._handleResponse(err, savedBoard, res)
    })
  })
},

Frontend

Back to the frontend, inside src/components/BoardPage.vue, add the following data property:

fromListIndex: null,

And the following methods:

onListDragStarted(fromListIndex) {
 this.$set(this, "fromListIndex", fromListIndex)
},
onListDragEnd(event) {
 this.$set(this, "fromListIndex", null);
},
onListDropped(toListIndex) {
 if (this.fromListIndex === toListIndex) {
  return;
 }
 this.switchListPositions(this.fromListIndex, toListIndex);
 this.updateListsOrder();
},
updateListsOrder() {
 let listIds = this.lists.map(list => list._id);
 boardService.updateListsOrder(this.board._id, listIds);
},
switchListPositions(fromListIndex, toListIndex) {
 if (this.fromListIndex === null) {
  return;
 }

 this.lists.splice(toListIndex, 0, this.lists.splice(fromListIndex, 1)[0]);
},

The methods starting with the on keyword will respond to events which happen when we drag a list item. To wire those methods, add the following lines to the created function:

this.$eventBus.$on("list-drag-started", this.onListDragStarted);
this.$eventBus.$on("list-dragend", this.onListDragEnd);
this.$eventBus.$on("list-dropped", this.onListDropped);

Update the board service src/services/board.service.js by adding in this method:

updateListsOrder(boardId, listIds) {
 return axios
  .put("/api/boards/updateListsOrder", {
   boardId: boardId,
   listIds: listIds
  })
  .then(res => res.data)
}

You may notice that we are using an instance property named $eventBus. It is responsible for sending messages across our application. It allows for any component at any level to subscribe to any broadcasted messages. Currently, it does not exist, let's wire it up.

Create a file in src/event-bus/index.js. Paste in the following:

import Vue from "vue"

export default new Vue()

Let's make it available to all Vue instances. Inside src/main.js, import it using:

import eventBus from './event-bus/index'

Then wire it up using the statement:

Vue.prototype.$eventBus = eventBus

It should now be available in every component.

Let's now modify the component src/components/List.vue. In there, in the top level template div node, after:

class="board-list"

Add in the following:

:class="{'is-dragging-list': isDraggingList, 'drag-entered': dragEntered}"
draggable="true"
@dragstart="onListDragStart(index, $event)"
@dragend="onListDragEnd"
@drop="onListDrop(index)"
@dragover.prevent
@dragover="onListDragOver"
@dragleave="onListDragLeave"

Then add the following handlers for the events above:

onListDragStart (fromIndex, event) {
 if (!fromIndex) {
  fromIndex = 0
 }
  
 this.$set(this, "isDraggingList", true)
 this.$eventBus.$emit("list-drag-started", fromIndex)
},
onListDragEnd () {
 this.$set(this, "isDraggingList", false)
 this.$eventBus.$emit("list-dragend")
},
onListDragOver (event) {
 this.$set(this, "dragEntered", true)
},
onListDragLeave (index, list) {
 this.$set(this, "dragEntered", false)
},
onListDrop (toIndex) {
 this.$set(this, "dragEntered", false)
 this.$eventBus.$emit("list-dropped", toIndex)
},

Add in the following data properties:

isDraggingList: false,
dragEntered: false

And, inside src/components/Board.vue, add the following data property:

fromListIndex: null,

Let's figure out what we are doing in the methods above. When we drag a list, it gets a class of is-dragging-list. This tilts the original list some degrees to show that it is the source. When a list is being dragged over, it gets a class of drag-entered. This adds a blue border around the list to show that it is currently being dragged over. When we release a list over another one, the original list takes the position of the one being dragged over. The target list gets shifted to the left.

Conclusion

That brings us to the end of this Task Manager Application. This is only a basic skeleton application. It has many of the basic elements for one to expand. We may have not implemented all the features. There is enough foundation in the code to create a more robust version of the application. Some features you could try to add are: delete feature for cards, lists and boards; draggable feature for cards within the same list or from one list to another; and finally to be able to edit the title for lists and cards as well as the card body.

Thanks For Visiting, Keep Visiting. If you like this tutorial please share it with others.