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

Publikováno: 13.4.2018

In the firstCelý článek

In the firstpart of this series, we looked at how to set up the backend server for the mini invoicing application. 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:

  • 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 chris@invoiceapp.com" v-model="model.email">
          </div>

          <div class="form-group">
              <label for="">Password:</label>
              <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password">
          </div>

          <div class="form-group">
              <button class="btn btn-primary" >Login</button>
              {{ loading }}
              {{ status }}
          </div>
      </form> 
  </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 chris@invoiceapp.com" v-model="model.email">
          </div>

          <div class="form-group">
              <label for="">Company Name:</label>
              <input type="text" required class="form-control" placeholder="eg Chris Tech" v-model="model.company_name">
          </div>
          <div class="form-group">
              <label for="">Password:</label>
              <input type="password" required class="form-control" placeholder="Enter Password" v-model="model.password">
          </div>
          <div class="form-group">
              <label for="">Confirm Password:</label>
              <input type="password" required class="form-control" placeholder="Confirm Passowrd" v-model="model.confirm_password">
          </div>
          <div class="form-group">
              <button class="btn btn-primary" >Register</button>
              {{ loading }}
              {{ status }}
          </div>
      </form>
  </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:

Sign Up + Header Component - Login

Sign Up + Header Component - Registering a New User

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 && price > 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:

Listing of Invoices with Payment Statuses

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.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace