Navigation and Authentication

Eighth Lesson in the Vue-Todo Series

Posted on August 1, 2016 in Laravel PHP Framework, Vue-Todo Series, vue.js

Navigation

We will be creating a new Vue Component for the Navigation on the site. The reason for this is that the navigation will need its own logic. We could add it to the App.vue file but I feel that would convolute a page which is already going to hold our auth logic.

Creating TopNav component

Create a new file called TopNav.vue here resources/assets/js/app/Components/Navigation/TopNav.vue and add the following:

<template>
    <nav class="navbar navbar-default navbar-fixed-top">
        <div class="container-fluid">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Vue-Todo-Series</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li v-link-active>
                        <a v-link="{path: '/', exact: true, activeClass: 'active'}">Home</a>
                    </li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    <li v-link-active>
                        <a v-link="{path: '/auth/login', exact: true, activeClass: 'active'}">Login</a>
                    </li>
                    <li><a href="#">Register</a></li>
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>
</template>

The code in TopNav.vue has mostly come from Bootstraps website. The main exception is the v-link and v-link-active. We've seen v-link before, but we've not seen v-link-active. As you can imagine this is a Vue placeholder that allows us to pick which element our active class will be applied to. The exact: true section of the v-link means that we want the URI to match exactly before applying the active class. Try removing it; once we've added the TopNav.vue component to our App.vue and see what happens. You should notice that when you go to the auth/login route, both the Login and Home links will be active. Now let's open resources/js/app/Components/App.vue and add the TopNav components.

<template>
    <div>
        <top-nav></top-nav>
        <div class="container-fluid">
            <router-view></router-view>
        </div>
    </div>
</template>

<script>
    import TopNav from './Navigation/TopNav.vue'

    export default {
        components: { TopNav }
    }
</script>

We have added a new element to the template section called <top-nav>. Now why have we added <top-nav> and not <TopNav>. Well, this is the convention used by Vue, and if you name a component SomethingLikeThis it will expect to find an element called <something-like-this>. One last thing to do is add some styling to our resources/views/welcome.blade.php to add some padding at the top of the body tag as we are using a fixed top navbar. Add this below the link to bootstrap CSS and above the </head> tag.

<style>
body {
    padding-top: 65px;
}
</style>

From your terminal run gulp and then open your browser and go to http://vue-todo-series.dev and you should see

Image of Artisan tinker

Authentication

What information are we likely to want to hold about our authenticated user. I think we would like to know if the user is authenticated, the user's details and the JWT token provided. So let's create these in our App.vue file so all our components will have access to this information. Add the following to resources/js/app/Components/App.vue files <script> section

import TopNav from './Navigation/TopNav.vue'

export default {
    components: { TopNav },
    data () {
        return {
            user: null,
            token: null,
            authenticated: false
        }
    }
}

Great, so how do we make use of any of this within our TopNav component. We will want to listen for any change to the authenticated value to display either the auth routes or the user routes. Let's make a few changes to the resources/js/app/Components/Navigation/TopNav.vue file

<template>
    <nav class="navbar navbar-default navbar-fixed-top">
        <div class="container-fluid">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Vue-Todo-Series</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li v-link-active>
                        <a v-link="{path: '/', exact: true, activeClass: 'active'}">Home</a>
                    </li>
                </ul>

                <ul class="nav navbar-nav navbar-right" v-if="!loggedIn">
                    <li v-link-active>
                        <a v-link="{path: '/auth/login', exact: true, activeClass: 'active'}">Login</a>
                    </li>
                    <li><a href="#">Register</a></li>
                </ul>

                <ul class="nav navbar-nav navbar-right" v-if="loggedIn">
                    <li><a href="#">Logout</a></li>
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>
</template>

<script>
    export default {
        computed: {
            loggedIn () {
                return this.$root.authenticated
            }
        }
    }
</script>

So, we have added a few new parts here.

  1. Added v-if to the right-hand nav section. What this is doing is checking the loggedIn value and will show one or the other if the user is authenticated
  2. Added a computed section. This section is where you can return values that need some computation. So in our case, we want to know if the user is authenticated or not.

Now run gulp and you should see that nothing has changed. But if you are using the Vue.js devtools Chrome app you can see that we now have a computed value within our TopNav component.

Image of Artisan tinker

Logging In

Let's now update our LoginPage component and see if we can login to our application. Open resources/assets/js/app/Components/Auth/LoginPage.vue and add the following:

<template>
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">Log into your account</h3>
                </div>
                <div class="panel-body">
                    <form role="form" v-on:submit.prevent="attempt">
                        <div id="alerts" v-if="alerts.length > 0">
                            <div v-for="alert in alerts" class="alert alert-{{ alert.type }} alert-dismissible" role="alert">
                                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                    <span aria-hidden="true">×</span>
                                </button>
                                {{ alert.message }}
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="email">Email address</label>
                            <input type="email" class="form-control" id="email" placeholder="Email"
                                   v-model="formUser.email">
                        </div>

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

                        <button type="submit" class="btn btn-primary" :disabled="loggingIn">
                            Login
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                formUser: {
                    email: null,
                    password: null
                },
                alerts: [],
                loggingIn: false
            }
        },

        methods: {
            attempt () {
                var that = this
                that.loggingIn = true
                that.$http.post('/api/login', that.formUser).then((response) => {
                    localStorage.setItem('jwt-token', response.json()['token'])
                    that.getUserData()
                }, (response) => {
                    that.alerts = []
                    if (response.status === 401) {
                        that.alerts.push({
                            type: 'danger',
                            message: 'Sorry, you provided invalid credentials'
                        })
                    }
                    that.loggingIn = false
                })
            },

            getUserData () {
                var that = this
                that.$http.get('/api/users/details').then((response) => {
                    that.$dispatch('userLoggedIn', response.json()['data'])
                    that.$route.router.go('/')
                }, (response) => {
                    console.log(response)
                })
            }
        },

        route: {
            activate (transition) {
                if (this.$root.authenticated) {
                    transition.redirect('/')
                }
                else {
                    transition.next()
                }
            }
        }
    }
</script>

Now, the <template> section is fairly straight forward. A few things to note, though.

  1. On the form tag we have v-on:submit.prevent="attempt". What this is doing is telling Vue that when the user submits the form prevent it from sending the information using the forms default method and instead use our attempt method. The .prevent means we don't need to pass the event to the method and also do not need to add event.preventDefault().
  2. We have a div tag that iterates over any errors we push into the array.
  3. We have :disabled="loggingIn" on our submit button. We add this as we don't want user's clicking multiple times if there is a slight delay in the response from the server.

Within our <script> section, we are creating our data which is relatively straight forward. We also now have our methods section. We have two methods in our LoginVue component. The first one is our attempt method. The method makes use of vue-resource to make a post request to our API and passes the formUser information. We then take the response from the server, which we know is the token on a successful login. We then set the token in the browser's local storage. We then call a method called getUserData(). If there are any errors returned we push an error to the alerts array.

The getUserData() method again makes use of the vue-resource dependency and makes a GET request to our API. This time, though we are dispatching the response to an event called userLoggedIn and passing the users data. So what is $dispatch('userLoggedIn'). This is a built-in method from Vue which allows us to communicate with other components. The $dispatch will send the event up through the chain of components until it comes to a component listening for the event. It will then stop unless the listener returns true in which case it will carry on. We will create the listener in our App.vue component shortly.

The last section is the route section which is added via vue-router. What we are doing to here is when the route is activated check if the user is authenticated. If yes, then redirect the user to the home page as they do not need to log in again. Otherwise show the LoginPage route.

Now let's create our listener in our resources/assets/js/app/Components/App.vue file. Add the following within the <script> section and after the data section

import TopNav from './Navigation/TopNav.vue'

export default {
    components: { TopNav },
    data () {
        ****
    },
    methods: {
        login: function (user) {
                this.user = user
                this.authenticated = true
                this.token = localStorage.getItem('jwt-token')
            },

            logout: function () {
                this.user = null
                this.token = null
                this.authenticated = false
                localStorage.removeItem('jwt-token')
                if (this.$route.auth) this.$route.router.go('/auth/login')
            }
    },
    ready () {
        this.$on('userLoggedIn', (user) => {
            this.login(user)
        })

        this.$on('userLoggedOut', () => {
            this.logout()
        })

        var token = localStorage.getItem('jwt-token')
        if (token !== null && token !== 'undefined') {
            var that = this
            that.$http.get('/api/users/details').then((response) => {
                that.login(response.json()['data'])
            }, (response) => {
                that.logout()
            })
        }
    }
}

We've added two new methods. The first is login which takes the user object and assigns it to App.vue data section. Updates the authenticated value and also assigns the token from local storage. The logout method does the opposite. We also have ready section. This section runs once the component is ready and loaded into the DOM. This is also where we have our listeners. These are the this.$on functions. We are also creating a new variable which is assigned the value of the token but from local storage. This is to check that it is still available and also check if it has expired by updating the user's details by making a GET request to our API. If any errors are returned, we call the logout method.

The last thing we need to do is tell Vue about vue-resource package. Open resources/assets/js/app/main.js and add the following to the import's: import VueResource from 'vue-resource'. Then add the following below Vue.use(VueRouter): Vue.use(VueResource). Now run gulp from your terminal. Now open your browser and reload http://vue-todo-series.dev and try logging in. You should hopefully see the following

Image of Artisan tinker

Oh, we have an error. It is saying that no token was provided to our API. If you check the Resources tab within your browser's developer overview and check the local storage, you will see that our login was successful, and we have a token set. What we haven't done is told vue-resource to add this token to our headers. This is a common error which is why I wanted to show it. Open your resources/assets/js/app/main.js and add the following below Vue.use(VueResource):

Vue.http.interceptors.push((request, next) => {
    request.headers['Authorization'] = 'Bearer ' + window.localStorage.getItem('jwt-token')

    next();
})

What we are doing here is telling vue-resource to add the Authorization header to each of our requests to the API. Now run gulp from your terminal again and try logging in again and you should see

Image of Artisan tinker

As you can see the login and register routes are gone and replaced with a logout route. Also if you look in the Vue devtools, you can see that our user object has our user's details. Great, now lets quickly add the LogoutPage component and RegisterPage component. These will be quick as most of the methods are very similar to ones already seen.

Create resources/assets/js/app/Components/Auth/LogoutPage.vue and add the following

<script>
    export default {
        route: {
            activate: function (transition) {
                this.$dispatch('userLoggedOut')
                transition.redirect('/')
            }
        }
    }
</script>

So, we are telling Vue that when this route is loaded send the userLoggedOut event.

Now create resources/assets/js/app/Components/Auth/RegisterPage.vue and add the following:

<template>
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">Register for a free account</h3>
                </div>
                <div class="panel-body">
                    <form role="form" v-on:submit.prevent="attempt">
                        <div id="alerts" v-if="alerts.length > 0">
                            <div v-for="alert in alerts" class="alert alert-{{ alert.type }} alert-dismissible" role="alert">
                                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                    <span aria-hidden="true">×</span>
                                </button>
                                {{ alert.message }}
                            </div>
                        </div>

                        <div class="row">
                            <div class="col-md-6">
                                <div class="form-group">
                                    <label for="first_name">First Name</label>
                                    <input type="text" class="form-control" id="first_name" placeholder="First Name"
                                           v-model="formUser.first_name">
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-group">
                                    <label for="last_name">Last Name</label>
                                    <input type="text" class="form-control" id="last_name" placeholder="Last Name"
                                           v-model="formUser.last_name">
                                </div>
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="email">Email address</label>
                            <input type="email" class="form-control" id="email" placeholder="Email"
                                   v-model="formUser.email">
                        </div>

                        <div class="row">
                            <div class="col-md-6">
                                <div class="form-group">
                                    <label for="password">Password</label>
                                    <input type="password" class="form-control" id="password" placeholder="Password"
                                           v-model="formUser.password">
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-group">
                                    <label for="password_confirmation">Confirm Password</label>
                                    <input type="password" class="form-control" id="password_confirmation"
                                           placeholder="Password" v-model="formUser.password_confirmation">
                                </div>
                            </div>
                        </div>

                        <button type="submit" class="btn btn-primary"  :disabled="registering">
                            Register
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                formUser: {
                    first_name: null,
                    last_name: null,
                    email: null,
                    password: null,
                    password_confirmation: null
                },
                alerts: [],
                registering: false
            }
        },

        methods: {
            attempt () {
                var that = this
                that.registering = true
                that.$http.post('/api/register', this.formUser).then((response) => {
                    localStorage.setItem('jwt-token', response.json()['token'])
                    that.getUserData()
                }, (response) => {
                    that.messages = []
                    if (response.status === 401 || response.status === 422) {
                        if (response.status === 422) {
                            for (var key in response.json()['errors']) {
                                if (response.json()['errors'].hasOwnProperty(key)) {
                                    that.messages.push({
                                        type: 'danger',
                                        message: response.json()['errors'][key]
                                    })
                                }
                            }
                        }
                        else {
                            that.messages.push({
                                type: 'danger',
                                message: 'Sorry, you provided invalid credentials'
                            })
                        }
                    }
                    that.registering = false
                })
            },

            getUserData () {
                var that = this
                that.$http.get('/api/users/details').then((response) => {
                    that.$dispatch('userLoggedIn', response.json()['data'])
                    that.$route.router.go('/')
                }, (response) => {
                    console.log(response)
                })
            }
        },

        route: {
            activate (transition) {
                if (this.$root.authenticated) {
                    transition.redirect('/')
                }
                else {
                    transition.next()
                }
            }
        }
    }
</script>

Now that we have our new components we need to create our routes for them and update the navigation. First open resources/assets/js/app/main.js and add the following to the imports at the top of the file:

import LogoutPage from './Components/Auth/LogoutPage.vue'
import RegisterPage from './Components/Auth/RegisterPage.vue'

Now update router.map({}) to the following:

router.map({
    '/': {
        component: HomePage
    },
    '/auth/login': {
        component: LoginPage
    },
    '/auth/logout': {
        component: LogoutPage
    },
    '/auth/register': {
        component: RegisterPage
    }
})

Now open resources/assets/js/app/Components/Navigation/TopNav.vue and update the navbar-right section to the following:

<ul class="nav navbar-nav navbar-right" v-if="!loggedIn">
    <li v-link-active>
        <a v-link="{path: '/auth/login', exact: true, activeClass: 'active'}">Login</a>
    </li>
    <li v-link-active>
        <a v-link="{path: '/auth/register', exact: true, activeClass: 'active'}">Register</a>
    </li>
</ul>

<ul class="nav navbar-nav navbar-right" v-if="loggedIn">
    <li><a v-link="{path: '/auth/logout'}">Logout</a></li>
</ul>

Now run gulp and start creating new users and logging out.

Next Time

In the next tutorial we will be looking at creating our Todo routes and functionality.


comments powered by Disqus