Lean Mean Vue Machine

Chris Berry JavaScript, Technology Snapshot, Vue Leave a Comment

The year is 2019 and Command Line Interfaces abound for many of the big name JavaScript frameworks such as Angular CLI, Create React App, and the Vue CLI.

But wouldn’t it be nice to go back to the days when you could just drop a simple script tag on a page and be able to run an application? Well, here is my attempt in trying to accomplish just that.

In this post, we create a working Vue.js web application with standard CRUD functionality and deploy it without any extra dependencies other than the actual application itself.

Application Goals

This application was created with a couple of goals in mind.

  1. Create a working Vue application without using a build system or module loader such as the
    Vue CLI or Webpack, yet still keep the templates and JavaScript together.

  2. Incorporate as much as the standard Create, Read, Update and Delete functionality as possible within the application while still keeping it simple and usable.

  3. Have the application be able to consume a third-party Application Programming Interface (API) hosted outside of the root of the User Interface application.

Structure of the Application

The application structure is actually quite simple and broken into two distinct sections: the UI and the API.

First the Application Server (or the API)

The API folder is a fully self-contained NodeJS application server that will load a selection of quotes up into an in-memory collection that will allow connecting parties to do the five main CRUD actives such as:

    /api/quote           | GET    | returns all quotes
    /api/quote/{id}      | GET    | returns a specific quote by 'id'
    /api/quote           | POST   | add a new quote
    /api/quote/{id}      | PUT    | update an quote
    /api/quote/{id}      | DELETE | deletes an quote

This server is CORS enabled, so all endpoints are open to public consumption and there is no security on the endpoints for simplicity. The quotes API itself does support proper HTTP verb endpoints.

[Server.js file]
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();

app.use(bodyParser.urlencoded({
   extended: true
}));

app.use(bodyParser.json());
app.use(cors());

const quotesApi = require('./quotes/');
quotesApi.register(app);

app.listen(process.env.PORT || 3412, () => {
   console.log('Cors-Enabled App listening on port 3412!');
});

The server file itself is pretty lean, where we only register the QuotesAPI after we have required the Quotes module. Once we have done that we can now do CRUD.

[quotes/index.js]
const data = require('./data.json');
const crud = require('./crudFunctions');

crud.loadData(data);

module.exports = {
   register: function (app) {

       app.get('/quote', (req, res) => {
           res.send(crud.getAll());
       });

       app.get('/quote/:id', (req, res) => {
           let quote = crud.getdById(req.params.id);
           if (!quote) {
               res.statusCode = 404;
               return res.send('Error 404: No quote found');
           } else {
               return res.send(quote);
           }
       });

       app.post('/quote', (req, res) => {
           // console.log(req.body);
           if (!req.body.hasOwnProperty('author') || !req.body.hasOwnProperty('text')) {
               res.statusCode = 400;
               return res.send('Error 400: Post syntax incorrect.');
           }

           let newQuote = {
               author: req.body.author,
               text: req.body.text
           };

           let data = {
               id: crud.addRow(newQuote)
           };
           res.send(data);
       });

       app.put('/quote', (req, res) => {
           if (!req.body.hasOwnProperty('author') || !req.body.hasOwnProperty('text')) {
               res.statusCode = 400;
               return res.send('Error 400: Post syntax incorrect.');
           }

           let existingQuote = {
               id: req.body.id,
               author: req.body.author,
               text: req.body.text
           };

           res.send(crud.updateRow(existingQuote));
       });

       app.delete('/quote/:id', (req, res) => {
           let found = crud.getdById(req.params.id);
           if (!found) {
               res.statusCode = 404;
               return res.send('Error 404: No quote found');
           } else {
               let data = {
                   count: crud.deleteRow(found)
               };
               res.send(data);
           }
       });
   }
}

As you notice here, there’s nothing too crazy going on. Just straight up HTTP requests using the proper verbs for the actions required. Do notice that we call crud.loadData(data); before any HTTP requests are even available. This is to ensure that the running server has its seed data ready to go the first time a request is made. Now onto the actual crud functions.

[quotes/curdFuctions.js]
let data = [];
var crudActions = {

   loadData: function (externalData) {
       data = externalData;
   },

   getAll: function () {
       return data;
   },

   getdById: function (id) {
       let found = data.find(item => item.id == id);
       if (found) {
           return found;
       } else {
           return undefined;
       }
   },

   addRow: function (item) {
       // find the highest ID associated with each element of the collection
       const highestId = Math.max.apply(Math, data.map(function (element) {
           return element.id;
       }));
       let newId = 1; // default incase the array is empty

       if (highestId > 0) {
           // generate a new ID based off of the highest existing element ID
           newId = (highestId + 1);
       }
       // make a new object of updated object.  
       let newItem = {
           ...item,
           id: newId,
           author: item.author,
           text: item.text
       };

       // insert the new item
       data.push(newItem);
       //console.log('New Element is: ', newItem);
       return newId;
   },

   updateRow: function (item) {
       const collectionCount = data.length;
       if (collectionCount > 0) {
           //find the index of object from array that you want to update
           const currentIndex = data.findIndex(element => element.id === item.id);

           //Log object to Console.
           //console.log("Before update: ", data[currentIndex])

           // make new object of updated object.  
           let updatedItem = {
               ...data[currentIndex],
               author: item.author,
               text: item.text
           };

           // update the array elemnent with the new data
           return data[currentIndex] = updatedItem;
       } else {
           return item;
       }
   },

   deleteRow: function (item) {
       let collectionCount = data.length;
       if (collectionCount > 0) {
           //find the index of object from array that you want to delete
           const currentIndex = data.findIndex(element => element.id === item.id);

           //Log count to Console.
           //console.log("Count before deletion: ", data.length);

           // remove from the array
           data.splice(currentIndex, 1);
           return collectionCount = data.length;
       } else {
           return collectionCount;
       }
   },

}

module.exports = crudActions

One of the goals of creating this file was to give each inserted item a real identifier when it came back from a server. Depending on indexes alone would cause problems. In this fashion, the data going in and coming out mimics a database in a much better way.

See Also:  Angular and Swagger: Experiences Learned

Last we have the actual JSON file.

[quotes/data.json]

   {
       "id": 5,
       "author": "Stephen King",
       "text": "Get busy living or get busy dying."
   },

This is just a small snippet but all of the rows followed this same fashion by having an ID, an AUTHOR, and a TEXT property.

Now for what we all came here for: the Vue application which will consume and use this applications server’s data.

The User Interface

From the beginning, we said we wanted to make this simple. No CLI, no Webpack, no SystemJS, no other style of build systems. Well, we’re in luck, Vue does still offer a way to just drop a script tag onto an HTML page and use its functionality right there. To me, that sounds like an advantage. So let’s see what it takes to make a functional application going that route.

For the UI directory, there are no actual dependencies needed to run the front end. Though you must be able to serve up the index.html file. There were two VS Code extensions I used for development which were instrumental in making this application easy to build.

Extension 1: Live Server

This extension will take any HTML page and serve it up from a hot reloadable web server. Any changes made will refresh the browser. Very much like the live-server npm package used with NodeJS, except you do not need node in your UI folder.

Extension 2: Template Literal Editor

Because this application did not use a build system for putting together the HTML and JavaScript for deployment, the template literal editor was used to give us the ability to edit our Vue HTML easily and without having to touch the actual literal templates. This worked out well because with a simple selection a new editor window opened with the HTML, changes could be made, the window would be closed and the template was updated.

Now onto the index page.

[index.html]
<!DOCTYPE html>
<html lang="en">

<head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <meta http-equiv="X-UA-Compatible" content="ie=edge" />
   <title>Quotes on Demand</title>
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" />
</head>

<body>

   <div class="container">
       <div id="app" v-cloak>
           <div class="shadow p-3 mb-5  rounded bg-dark text-light">
               <span class="float-right">
                   <router-link to="/" class="btn btn-outline-light" active-class="active" exact> Quote Listing</router-link>
                   <router-link to="/manage" class="btn btn-outline-light" active-class="active"> Manage Quotes</router-link>
               </span>
               <h2 class="display-5">Quotes on Demand</h2>
           </div>

           <router-view></router-view>
       </div>
    </div>

   <!-- vue application dependencies -->
   <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
   <script src="https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.js"></script>
   <script src="https://unpkg.com/http-vue-loader"></script>
   <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

   <!-- vue application scripts -->
   <script src="./pages/home.vue.js"></script>
   <script src="./pages/manage.vue.js"></script>

   <script>
       const baseUrl = 'http://localhost:3412';

       Vue.use(VueRouter);

       const routes = [{
               path: '/',
               component: home
           },
           {
               path: '/manage/:id?',
               component: manage
           }
       ];

       let router = new VueRouter({
          routes // short for `routes: routes`
       });

       router.beforeEach((to, from, next) => {
           next()
       });

       var app = new Vue({
           el: '#app',
           watch: {},
           mounted() {

           },
           data: {
               msg: 'Hello',
               email: ''
           },
           methods: {},
           router
       });
   </script>
</body>

</html>

As you can see, it’s a fairly straight forward Vue application. This application will need to hit an outside service for data, so I’m going to always assume the internet is available and use a Content Delivery System to load the necessary Vue dependencies. These are referenced at the bottom of the markup and come before the actual Vue application.

Since I wanted to demonstrate as much as possible of Vue and Vue Router, we have created two pages, one for the root which loads upon startup, and a second page called manage which looks for an ID on its route and allows the user to add or edit a quote.

[pages/home.vue.js]
var home = Vue.component("Home", {
 template: `<div>
   <h 4>{{message}}</h 4>
   <hr />
   <p>Select Quote: <select @change="onChange($event)">
           <option v-for="item in selectList" :value="item.value">{{item.author}}</option>
       </select>
   </p>
   <hr />
   <div v-for="(quote, index) in quotes" :key="index" v-if="multiple">
       <blockquote class="blockquote">
           <p class="mb-0 col-11 .col-md-7">{{ quote.text }}</p>
           <footer class="blockquote-footer">{{quote.author}}
               <div class="float-right">
                   <router-link :to="'/manage/' + quote.id" class="btn btn-secondary btn-sm">edit</router-link>
                   <button type="button" title="Delete Quote" class="btn btn-outline-danger btn-sm" @click="removeSingleQuote(quote.id)">
                       <span>&times;</span>
                   </button>
               </div>
           </footer>
       </blockquote>
       <hr />
   </div>

   <div v-if="!multiple">
       <blockquote class="blockquote">
           <p class="mb-0 col-11 .col-md-7">{{ quote.text }}</p>
           <footer class="blockquote-footer">{{quote.author}}
               <div class="float-right">
                   <router-link :to="'/manage/' + quote.id" class="btn btn-secondary btn-sm">edit</router-link>
                   <button type="button" title="Delete Quote" class="btn btn-outline-danger btn-sm" @click="removeSingleQuote(quote.id)">
                       <span>&times;</span>
                   </button>
               </div>
           </footer>
       </blockquote>
   </div>
</div>`,
 data() {
   return {
     message: 'View our quotes',
     quotes: [],
     quote: {},
     multiple: false,
     selectList: [],
     error: '',
   };
 },

 methods: {
   onChange(event) {
     let quoteIndex = event.target.value;
     if (quoteIndex == 0) {
       this.getAllQuotes();
     } else {
       this.getSingleQuote(quoteIndex);
     }
   },

   getAllQuotes() {
     axios
       .get(baseUrl + '/quote/')
       .then(response => {
         let data = response.data;
         this.quotes = data;
         this.selectList = this.setupSelect(data);
         this.selectList.unshift({
           author: 'All Authors',
           value: 0
         });
         return (this.quotes, this.selectList);
       });

     this.multiple = true;
   },

   getSingleQuote(id) {
     axios
       .get(baseUrl + '/quote/' + id)
       .then(response => (this.quote = response.data));
     this.mulitiple = false;
   },

   removeSingleQuote(id) {
     axios.delete(baseUrl + '/quote/' + id)
       .then(() => {
         this.getAllQuotes()
       })
       .catch((error) => {
         this.$log.debug(error);
         this.error = "Failed to remove quote"
       });

   },

   setupSelect(quoteList) {
     return quoteList.map(function (quote) {
       return {
         author: quote.author,
         value: quote.id
       };
     }).sort((a, b) => (a.author > b.author) ? 1 : -1);
   }
 },

 mounted() {
   this.getAllQuotes();
 }
});

This is a home Vue page. Right off the bat, we’re making a Vue.component adding the template, which is wrapped as an ES6 template literal. This is where the editor extension comes in handy. After the template, we have the data, methods, and a mounted which allows the quotes to load as soon as the page is completely rendered.

See Also:  AWS SNS Push Notifications

In this page we’re basically just using simple Axios Ajax calls to demonstrate the GetAll and GetById methods along with a DeleteById. Each quote has a computed link that attaches its own ID to a route. This allows us to edit the quote. Which leads us to the manage page.

[ui/manage.vue.js]
var manage = Vue.component("Manage", {
 template: `<div>
  <h 4>{{ message }}</h4>
  <hr />
   <form>
       <input type="hidden" v-model="quote.id" />
       <div class="form-group">
           <label for="formGroupAuthorInput">Author:</label>
           <input type="text" class="form-control" id="formGroupAuthorInput" placeholder="Author Name" v-model="quote.author">
       </div>
       <div class="form-group">
           <label for="formGroupQuoteTextInput">Quote Text</label>
           <input type="text" class="form-control" id="formGroupQuoteTextInput" placeholder="Quote text" v-model="quote.text">
       </div>
<button type="submit" class="btn btn-outline-success" @click="save">Save Quote</button>
       <button type="button" class="btn btn-danger" @click="cancel">Cancel</button>
   </form>
</div>
`,

 data() {
   return {
     message: 'Add or update a quote',
     quote: {
       id: undefined,
       author: '',
       text: ''
     },
     success: false,
     error: ''
   };
 },
 methods: {
   load() {
     let id = this.$route.params.id;
     if (id) {
       axios
         .get(baseUrl + '/quote/' + id)
         .then(response => (this.quote = response.data));
     }
   },

   save() {
     if (this.quote.author && this.quote.text) {
       if (this.quote.id) {
         axios.put(baseUrl + '/quote/', this.quote)
           .then(function (response) {
             this.success = response.data;
             if (this.success) {
               router.push('/');
             }
           }).catch(function (error) {
             this.error = error;
           });
       } else {
         axios.post(baseUrl + '/quote/', this.quote)
           .then(function (response) {
             this.success = response.data;
             if (this.success) {
               router.push('/');
             }
           })
           .catch(function (error) {
             this.error = error;
           });
       }
     }
   },

   cancel() {
     router.push('/');
   }
 },

 mounted() {
   this.load();
 }
});
 

The managed page is a simple form. If you clicked edit, you navigated here with an attached ID which was found in this.$route.params.id. This ID is used to do a quick lookup of the quote and load the Author and Text into the form fields found on the page. If you used the Manage Quotes link at the top of the page, no ID was passed along and the form is treated as a new quote. When saving the form, the this.quote.id is used to determine which Axios action verb to call.

A Few Notes

You do not need to have the live server extension installed to run the UI project. You can use any method you would like to serve up static files to a browser. This could include using a NodeJS server to serve static files, or you can use a LAMP server or even a local instance of IIS.

That’s a Wrap Folks!

We did it. That was the goal of this project, to be able to deploy a Vue web application without any extra dependencies other than the actual application itself. We succeeded!

What Do You Think?