In this part, we’ll be implementing three small, yet important features in our app.
First, we’ll apply pagination to our topics. Then, we’ll see how to display a beautiful breadcrumb to show users where they are in the forum’s hierarchy. Finally, we’ll see how easy it’s to show a loading indicator when moving between pages.
Obviously, we don’t want to display all topics in our forum all at once; we need to break them up into multiple pages using pagination.
To achieve that, we need to do two things. First, we need to modify our topics
end point to paginate the data before sending them — that’s on the server side. Then, on the client side, we’ll include the page number in the request and display the pagination buttons.
So in CategoryController
replace everything in topics
method with this (read the comments for explanation):
public function topics($categoryId)
{
// The only change here is that we retrieve topics
// with paginate() instead of get().
// Note that the number of items per page is 3
// Also note that the page number (in the queryString)
// is automatically detected by Laravel.
$topics = Category::findOrFail($categoryId)
->topics()
->orderBy('created_at', 'desc')
->paginate(3);
// Return an error message if it's empty
if ($topics->isEmpty()) {
return response('The provided page exceeds the available number of pages', 404);
}
// Return the paginated topics
return $topics;
}
That’s all you have to do on the server side. (Thanks Laravel for making it so easy!)
Now, let’s move to CategoryView
to make this pagination work.
Obviously, we’ll display our pagination buttons bellow the topics. So, add this html code under the closing </topic>
:
<div class="Pagination">
<a
v-if="+page-1 >= 1"
v-link="{ name: 'category', params: { categoryId: categoryId }, query: { page: +page - 1 }}"
class="Page-link Page-link--prev">
Back
</a>
<a
v-if="+page+1 <= lastPage"
v-link="{ name: 'category', params: { categoryId: categoryId }, query: { page: +page + 1 }}"
class="Page-link Page-link--next">
Next
</a>
</div>
We’re displaying two buttons here — next and back buttons. But before displaying them, we needed to check whether they are valid in the current page. For instance, we don’t want to display the “next” button if we’re already on the last page — that’s what the v-if
is for.
Then, in the v-link
, we specified the url for each one, which would ultimately be translated into something like this:
/category/{categoryId}?page={pageNumber}
.
Next, we need to modify our route
transition to include the page number in the request.
route: {
data (transition) {
// Save the categoryId for the pagination buttons above
this.categoryId = +transition.to.params.categoryId;
this.page = +transition.to.query.page || 1;
// Include the page number into the query string
return this.$http.get(`/api/categories/${this.categoryId}/topics`, { page: this.page })
.then(({data}) => ({
topics: data.data,
lastPage: data.last_page
}));
}
}
Note how we needed to save the last page number so we can determine if the “next” button should be displayed — remember the v-if
above?
Lastly, we need to register those data variables in our component.
data () {
return {
topics: [],
categoryId: 0,
page: 1,
lastPage: 1
}
}
You should end up with something like this:
I haven’t yet seen any forum without a breadcrumb in it; so why not add one to ours!
In case you don’t know what a breadcrumb is (who doesn’t know that, anyway?), a breadcrumb is a series of links that indicate how you arrived to the current page starting from the home page. Here’s how ours will look like:
There are multiple ways we can implement it, but I prefer to push the responsibilities to the server to make it extremely easier on our client.
By pushing responsibilities to the server, I mean we won’t maintain the current state of our breadcrumb component in the client side. Instead, we receive the whole links tree from the server and just display it.
So, let me show you how it’s done.
In Topic.php
, add a breadcrumb
field to both $visible
and $appends
, and, of course, the corresponding accessor method — getBreadcrumbAttribute
.
protected $visible = ['id', 'title', 'body', 'views', 'time', 'category_id', 'breadcrumb'];
protected $appends = ['time', 'breadcrumb'];
// …
public function getBreadcrumbAttribute()
{
$category = $this->category;
return [
[ 'title' => 'Home', 'link' => '/' ],
[ 'title' => $category->name, 'link' => "/category/$category->id"],
[ 'title' => $this->title, 'link' => "/topic/$this->id"]
];
}
This means, every time we receive topics from our server, we’ll see the whole links tree included there. And since topics are the deepest level in our app, it would be sufficient to us when we use it in other levels — categories and home.
That’s it for the API.
Now, let’s switch to App.vue
and display the breadcrumb component, which we don’t have yet, under the header in App.vue
:
<header class="Header">
<h1 v-link="'/'" class="Header__logo">
SPA-FORUM <small>with vue.js</small>
</h1>
</header>
<breadcrumb :breadcrumb="breadcrumb"></breadcrumb>
<!— … —>
<script>
import Breadcrumb from './Breadcrumb.vue';
export default {
components: { Breadcrumb },
data () {
return {
breadcrumb: []
}
}
}
</script>
Now let’s create that component in components/Breadcrumb.vue
:
<template>
<ol class="Breadcrumb">
<li class="Breadcrumb-item" v-for="item in breadcrumb">
<a class="Breadcrumb__link" v-link="{ path: item.link }" v-if="!currentPage(item.link)">
{{ item.title }}
</a>
<span class="Breadcrumb__link Breadcrumb__link--inactive" v-else>
{{ item.title }}
</span>
</li>
</ol>
</template>
<script>
export default {
props: ['breadcrumb'],
methods: {
currentPage (link) {
return this.$route.path.split('?')[0] == link;
}
}
}
</script>
<style lang="stylus">
.Breadcrumb
list-style: none
display: flex
margin: 0
margin-bottom: 15px
padding: 0
padding-left: 20px;
margin: 25px 0;
.Breadcrumb:before
content: ' '
display: block
width: 3px
background: #f0f0f0
margin-right: 8px
.Breadcrumb-item
&:first-child:before
display: none
&:before
content: '/'
padding-left: 15px
padding-right: 15px
color: gray
.Breadcrumb__link
color: deepskyblue
text-decoration: none
&--inactive
color: gray
</style>
Nothing is so special about this component, we’re just displaying the links. Also, we’re checking whether each one needs to be active (clickable) or not. Obviously, it shouldn’t be active if it’s for the current page.
Now we come to the important part. Since we have centralized the breadcrumb data in the root component — App.vue
— we need a way to update its data from other page components.
To do that, we have two options, either by setting the value directly using this.$root.breadcrumb = breadcrumb
, or by emitting an event containing that value. For me, I prefer the latter.
Let’s apply that for each category page. But the main question is, when should we update that breadcrumb component’s data? The answer is pretty easy: every time the topics array is updated; remember that’s where we get that breadcrumb data from.
And by saying “every time the topics array is updated”, I mean we should use a watcher! So, in CategoryView
, add this watcher for topics:
watch: {
topics () {
// Get breadcrumb from any topic object
// since they all have the same value.
// What about the first one?
let breadcrumb = this.topics[0].breadcrumb;
// Remove the last link in the breadcrumb
// because it's for the topic and we're
// here in the category page
breadcrumb.pop();
// Lastly, emit that event with the breadcrumb data
this.$emit('update-breadcrumb', breadcrumb);
}
}
In order for that to work, we have to listen for this event in the <router-view>
component:
<router-view
class="view"
keep-alive
transition
transition-mode="out-in"
@update-breadcrumb="updateBreadcrumb"
>
Then add the corresponding method:
methods: {
updateBreadcrumb (breadcrumb) {
this.breadcrumb = breadcrumb
}
}
Now if you check any category page, you’ll see the breadcrumb displayed as expected.
The last thing we need to do in this section is to tell HomeView
about our breadcrumb so it can also display it. But we’ll display a different title there, because it makes more sense to display “Categories” instead of “Home”.
For this case, we would need to update the breadcrumb inside the activate
transition hook, because we want it to be updated every time we visit that page — not only once, as in ready
method. So in HomeView
add an activate
transition hook under the route
object, like this:
route: {
activate () {
this.$emit('update-breadcrumb', [{
title: 'Categories',
link: '/'
}]);
},
That’s it! Now you should see “Categories” in the homepage.
Unlike traditional apps, in SPAs we don’t make a full reload for each page we visit. But we might end up seeing a blank page when it’s loading. So, we usually avoid that by showing a loading indicator — So, let’s add one!
The cool thing is that it’s pretty easy to do, especially with the library.
Let’s install it first:
npm install nprogress --save
Next, let’s tell elixir to mix its styling into our all.css
. So, update your gulpfile.js
:
elixir(function(mix) {
mix.browserify('main.js')
.styles([
'./node_modules/normalize.css/normalize.css',
'./node_modules/nprogress/nprogress.css'
]);
});
All you need to know about NProgress is that you need to call two methods on it: NProgress.start()
and NProgress.done()
. The first one is to start the loading and the other one is to, well, finish it!
But where should we call them? Obviously, we need to start it when the request is sent, and finish it when we receive the response. And this is where I’ll introduce you to HTTP Interceptors.
HTTP interceptors are, simply, what allow us to do any necessary work before the request is sent and after the response is received — and it’s for all calls made in the app.
The one we’re using here is part of the vue-resource
library. To use them, write this code before router.start(App, 'app')
in main.js
.
Vue.http.interceptors.push({
request (request) {
// Start the progress bar
NProgress.start();
return request;
},
response (response) {
// Finish the progress bar
NProgress.done();
return response;
}
});
And of course don’t forget to import NProgress
at the top:
import NProgress from 'nprogress'
That’s it! Now you should see the progress bar when navigating between pages.
With that, this series has come to the end. I know you probably thought there would be more to cover. You're right! But at this point, I don't think spending more time and energy on this series would be very helpful for several reasons:
In the end, I'd like to thank you all for reading this series and for the awesome feedback I received about it. And I hope I've added something valuable to the community. I would be even more happy to answer any questions you have about this series or about Vue in general.
I'm a freelance web developer. Laravel & VueJS are my main tools these days and I love building stuff using them. I write constantly on this blog to share my knowledge and thoughts on things related to web development... Let's be friends on twitter.