DEV Community

loading...

Build a Quote Application Using Laravel and Vue: Part 2

Oluyemi
A tech enthusiast, programming freak, and web development junkie.
Originally published at Medium on ・9 min read

Quote Application Part 2

In the first part of this series, we were able to successfully build the backend of our Quote Application using Laravel. We went ahead to also set up endpoints to post, fetch, update, and delete quote from our database. And lastly, we tested the functionality of our API by using a tool called Postman.

In this article, we will complete the Quote application, by building the frontend with Vue.js. Here, we want to be able to achieve the following :

  1. Post a new quote to the server
  2. Retrieve all quotes after they have been saved
  3. And finally, edit and delete quotes.

Just before we proceed, open up the source code from the first part of the series in a code editor of your choice and run the application. It is important to keep this running as one of the objective of this tutorial is to ensure smooth communication between the Backend and Frontend on separate domains. The complete code for both application can be found on Github, scroll to the end of this tutorial to access the links.

Let’s Build the Frontend

An HTTP call will be required to access all the resources from the backend. For this, we will make use of Axios which is a Promised based HTTP Client for the browser and node.js, but first, let’s install Vue. Vue-cli will be used here, as it will help us to rapidly scaffold Single Page Application in no time.

# install vue-cli
$ npm install -g vue-cli

Next, we’ll set up our Vue app with the CLI.

# create a new project using the "webpack" template
$ vue init webpack-simple frontend-quote-app

You will be prompted to enter a project name, description, author and others. This should initialize our app, all we have to do now is, change directory into our project folder and install the required dependencies.

#change directory
$ cd frontend-quote-app

#install dependencies
$ npm install

Finally, to serve the application, run

# run the application
$ npm run dev

A similar page like the image below should open up in your browser by now

Components

Since Vue offers developers the ability to use components driven approach when building web apps, we will create more components for our quote application. Vue CLI already generated a main component that can be found in src/App.vue, this will be used as the top level component for our application.

Creating a Component

Apart from the default component generated by Vue CLI, we will need more component, namely, 'new-quote.vue', quotes.vue', quote.vue' . These components will be used to add a new quote, display all quotes and moreso, be able to edit and delete quote.

Ready? Let’s get to work!.

Proceed to create a ./src/components folder, which will hold all the components that we will create soon.

Create more component JS files such as quote.vue , quotes.vue , new-quote.vue within the components folder.

Install NPM Modules

As we are required to make web requests (API calls) within all the components created above, install Axios).

npm install axios --save

And for routing purposes, let’s also install Vue-router

npm install vue-router --save

Configure Components

The required tools and components files have just been created, next is to start configuring these files by creating individual template, logic and style.

First of all, clean up the default contents within ./src/App.vue . This will be filled later.

<template>

<div id="app">
...
</div>

</template>

<script type="text/babel">

export default {

data () {

return {

}
  }
}
</script>

<style lang="scss">
...
</style>

New-quote.vue

This component is responsible for adding new quote(s). Whenever the form for posting a new quote is submitted, a function 'onSubmitted will be called and executed.

<template>
   <div>
        <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
            <form @submit.prevent="onSubmitted">
                <div class="form-group">
                    <label for="content">
                        <b>Quote</b>
                        </label>
                        <br>
                    <i> Write your quote</i>

                    <textarea name="" id="content" class="form-control" v-model="quoteContent" cols="80" rows="6"></textarea>
                </div>

                <div class="form-group">
                    <button type="submit" class="btn btn-success">
                        Submit
                    </button>
                </div>
            </form>
        </div>
    </div>
</template>

<script type="text/babel">
    ...
</script>

<style scoped>
...
</style>

This function sends an HTTP request to the server (Laravel backend) with the quote data and stores it in the database. This pattern is similar to what we have in other components as will be revealed very soon.

<template>
...
</template>
<script type="text/babel">
    import axios from 'axios';
    export default {
        data() {
            return {
                quoteContent: ''
            }
        },
        methods: {
            onSubmitted() {
                axios.post('http://localhost:8000/api/quote',
                        {content: this.quoteContent})
                        .then((response) => {
                        window.location.href = "/";
                        })
            .catch ((error) => console.log(error)
            )}
        }
    }
</script>

<style scoped>
...
</style>

Notice the URL and endpoint being called by axios within the onSubmitted() method http://localhost:8000/api/quote? Remember the Laravel backend was started at the beginning of this tutorial, it is assumed that this application is running on localhost port 8000. Kindly change this URL if your backend is running on a different port.

And style

<style scoped>
#content {
    margin-top: 40px;
}
</style>

Quotes.vue

This is the parent component for quote component. Props defined here are are used to passed down information to the child component.

<!-- quotes.vue -->
<template>
    <div>
        <div class="text-center">
            <button class="btn btn-success" @click="onGetQuotes">
            Get Quotes
             </button>
        </div>
        <hr>

<app-quote v-for="quote in quotes" :qt="quote" :key="quote.id" @quoteDeleted="onQuoteDeleted($event)"></app-quote>
    </div>
</template>

<script type="text/babel">
    ...
</script>

onGetQuotes() will initiate a call to the API backend and return all the posted quote(s) as response. This is being called after the instance has been mounted.

<script type="text/babel">
    import Quote from './quote.vue';
    import axios from 'axios';

export default {
        data() {
            return {
                quotes: []
            }
        },
        methods: {
            onGetQuotes() {
                axios.get('http://localhost:8000/api/quotes')
                        .then(
                                response => {
                    this.quotes = response.data.quotes;
                }
                        )
                .catch(
                        error => console.log(error)
                );
            },
            onQuoteDeleted(id) {
                const position = this.quotes.findIndex((element) => {
                            return element.id == id;
                        });
                this.quotes.splice(position, 1);
            }
        },
        mounted: function () {
           this.onGetQuotes();
        },
        components: {
            'app-quote':Quote
        }
    }
</script>

Quote.vue

This is the child component of quotes component. Conditional declaration is used to toggle the editing mode, when the edit button is clicked, an onEdit() method is called and editing property from the vue instance is set to true. on the other hand, the onCancel() and onUpdtae() methods will set editing property to true after the specified login for this function are carried out.

<!-- quote.vue -->
<template>
<div>
    <div v-if="editing">
        <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
            <div class="form-group" id="form__group">
                <label for="content"><b>Edit Quote</b></label><br>
                <textarea id="content" v-model="editValue" rows="10" cols="30" class="form-control"></textarea>
                <div class="control_1">
                    <button @click="onUpdate" class="btn btn-success">Save</button>
                    <button @click="onCancel" class="btn btn-danger">Cancel</button>
                </div>
            </div>
        </div>
    </div>

<div v-if="!editing">
        <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
            <div class="quote-holder">
                <div class="quote">
                    {{ qt.content }}
                </div>

<div class="quote_control">
                    <div>
                        <div class="control_1">
                            <button @click="onEdit" class="btn btn-primary">
                                Edit
                            </button>
                            <button @click="onDelete" class="btn btn-danger">
                                Delete
                            </button>
                        </div>

<div class="control_2">
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

</template>

<script type="text/babel">
   ...
</script>

<style scoped>
   ...
</style>

The expected information, in this case, quote are declared and received using the props option :

<script type="text/babel">
    import axios from 'axios';

export default {
        props: ['qt'],
        data() {
            return {
                editing: false,
                editValue: this.qt.content
            }
        },
        methods: {
            onEdit() {
                this.editing = true;
                this.editValue = this.qt.content
            },
            onCancel() {
                this.editing = false;
            },
            onDelete() {
                this.$emit('quoteDeleted', this.qt.id);
                axios.delete('http://localhost:8000/api/quote/' + this.qt.id)
                        .then(
                                response => console.log(response)
            )
            .catch (
                        error => console.log(error)
            )
            },
            onUpdate() {
                this.editing = false;
                this.qt.content = this.editValue;
                axios.put('http://localhost:8000/api/quote/' + this.qt.id,
                        {content: this.editValue})
                        .then(
                                response => console.log(response)
            )
            .catch (
                        error => console.log(error)
            )
                ;
            }
        }
    }
</script>

Style

<style scoped>
a {
        cursor: pointer;
    }

.quote {
        display: block;
        margin-left: auto;
        margin-right: auto;
        /*min-height: 125px;*/
    }

.quote-holder {
        background: #ffffff;
        margin-bottom: 30px;
        position: relative;
        overflow: hidden;
        padding: 20px;
        min-height: 250px;
    }
    .quote_btn {
        border-radius: 0;
        width: 100%;
        display: block;
        cursor: pointer;
    }

.quote_control {
        width: 100%;
        display: flex;
        padding: 20px 20px 15px;
        background: #FFF;
    }

.control_1 {
        flex: 2;
    }
    .control_2 {
        flex: 1;
        /*display: flex;*/
        justify-content: flex-end;
        align-items: center;
        font-size: 20px;
        font-weight: bold;
        color: #51D2B7;
    }

#form__group{
        box-sizing: border-box;
        overflow: hidden;
    }

textarea {
        margin: 10px 0;
    }
</style>

index.html

Bootstrap classes are used to improve the styling in this application. Don’t forget to include stylesheet in index.html file :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue + laravel</title>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

</head>
  <body>
    <div id="app"></div>
    <script src="/dist/build.js"></script>
  </body>
</html>

App.vue

Earlier we cleaned up this file by getting rid of the default contents. Now, fill it with :

<!-- App.vue -->
<template>
  <div id="app">
    <div class="container">
      <div class="row">
        <div class="col-xs-12">
         <nav class="navbar navbar-default navbar-fixed-top">
            <div class="container">
              <ul class="nav navbar-nav navbar-center links">
                <li><router-link to="/">Quotes</router-link></li>
                <li><router-link to="/new-quote"> New Quotes</router-link></li>
              </ul>
            </div>
          </nav>
        </div>
      </div>
      <hr>
      <div class="row">
        <div class="col-xs-12">
          <div id="view">
            <router-view></router-view>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script type="text/babel">
export default {
  data () {
    return {

}
  }
}
</script>

<style lang="scss">
#app {
  margin: 30px 0 0 0;
  background: #F7F8FB;
  min-height: 800px;
}

#view {
    margin-top: 80px;
  }

.navbar {
    background: #333333;
    min-height: 70px;
    font-weight: bold;
  }
  .links {
   margin-top: 10px;
  }

.links li a {
    color: #ffffff !important;
    font-weight: bold;
    font-size: 20px;
  }
</style>

Don’t forget that we have made use of custom html tags within our vue' files. All these component tag and routes will be managed by src/main.js . So open the file and fill in the content below :

<!-- src/main.js -->
import Vue from 'vue'
import VueRouter from 'vue-router';

import App from './App.vue'
import Quotes from './components/quotes.vue';
import NewQuote from './components/new-quote.vue';

Vue.use(VueRouter);

const routes = [
  { path: '', component: Quotes},
  { path: '/new-quote', component: NewQuote },
];

const router = new VueRouter({
  node: 'history',
  routes: routes
});
new Vue({
  el: '#app',
      router: router,
  render: h => h(App)
})

CORS (Cross-Origin Resource Sharing)

Now if we attempt to post a quote from our frontend-quote-app, we will be redirected to the homepage but the quote will not be saved. Inspecting the browser will reveal what the error is.

Check the console

Just before you get scared, it is interesting to affirm that we had this error coming. In this application, we are trying to create a direct connection between two different domains. Technically, It is generally not allowed to have two different applications, with different domain name exchanging data. This is by default a form of security measure, but since we are building an API backend, we will have to turn off this protection in order to allow our frontend communicate effectively with the backend.

Back to the Backend Application

Earlier on, I stated that we need to keep our server on, just incase you haven’t done that.

Mission

Our goal, is to be able to target all API route from a different domain. In order to achieve this, we must create a new middleware, register it and eventually attach this middleware to our routes. You can read more about Laravel middlewares here.

Create Middleware

Creating a middleware in Laravel application is quite easy. A middleware named Cors will be created for the purpose of this article, and all we have to do is run this command. So open your terminal and go ahead :

php artisan make:middleware Cors

This will create a middleware in app/Http/middleware/Cors.php . Now is the convenient moment to open the newly created middleware and add the logic to accept incoming HTTP request from the frontend-quote-app'.

<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{

    public function handle($request, Closure $next)
    {
        return $next($request)
            ->header('Access-Control-Allow-Origin', '*')
            ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
            ->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

Here, HTTP headers were set to allow the frontend-application gain permission to access resources from the backend app.

Now that we have added the required Logic into the created middleware, Laravel needs to know that a new middleware exists and then be able to use it. Go to app/Http/Kernel.php and add to kernel class :

protected $middleware = [
       ...
        \App\Http\Middleware\Cors::class,
    ];

With this, we should be able to successfully communicate with our backend application.

Please feel free to try out the demo here.

Conclusion

So in this series, we have learnt to :

* Build API using Laravel

* Use a frontend library (VueJs) to consume the API built with Laravel

* Ensure smooth communication between two separate domains.

I hope you have seen how you can conveniently connect Vuejs with a Laravel API backend.

In a subsequent post, we will learn how to add authentication to this application and allow only user with the right access to be able to carry out certain actions like editing and deleting a particular quote.

If you found this tutorial helpful, have any suggestions or encounter any issues, kindly leave a comment below.

Here are the links to the source code in both series can be found on github, Backend and Frontend.

Discussion (0)