Creating an infinite scroll with Vue for WordPress
Introduction
If you ever want to create an infinite scroll with Vue for your WordPress projects, here is a working example. The process can be a bit tedious but it will be quick and smooth once you have the template set up.
WordPress API
First of all, you need to create a custom JSON API endpoint in funtions.php
to get the custom posts data from WordPress database and then pass the JSON output to Vue object. For example, I have this block of code in funtions.php
to fetch the posts with publication
post-type:
function api_publications($data) {
$output = array();
// e.g. 1, 2, 3,...
$paged = $data['page_number'] ? $data['page_number'] : 1;
$lang = $data['lang'];
$query_args = array(
'post_type' => 'publication',
'post_status' => array('publish'),
'posts_per_page' => 4,
'paged' => $paged,
'orderby' => 'date',
);
// Create a new instance of WP_Query
$the_query = new WP_Query($query_args);
if (!$the_query->have_posts()) {
return null;
}
while ($the_query->have_posts()) {
$the_query->the_post();
$post_id = get_the_ID();
// Get the slug.
// https://wordpress.stackexchange.com/questions/11426/how-can-i-get-page-slug
$post = get_post($post_id);
$post_slug = $post->post_name;
$post_title = translateTitle($post, $lang);
$post_date = get_the_date('j M Y');
$post_url = switchLink(get_permalink($post_id), $lang);
// Get the taxonomy that attached to the post.
$taxonomy = get_the_terms($post, 'publication-source');
// Push the post data into the array.
$output[] = array(
'id' => "post" . $post_id, // cannot use numbers, or hyphens between words for Zurb data toggle attribute
'slug' => $post_slug,
'title' => $post_title,
'url' => $post_url,
'date' => $post_date,
'taxonomy' => $taxonomy
);
}
// Reset the post to the original after loop. otherwise the current page
// becomes the last item from the while loop.
wp_reset_query();
return $output;
}
add_action('rest_api_init', function () {
register_rest_route('publications/v1', '/lang/(?P<lang>[a-zA-Z0-9-]+)/parent/(?P<parent_id>\d+)/page/(?P<page_number>\d+)', array(
'methods' => 'GET',
'callback' => 'api_publications',
));
});
You can test the endpoint on your browser, it should return the similar JSON data below:
[{
"id": "post324",
"slug": "sample-publication-12",
"title": "Sample Publication 12",
"url": "http:\/\/127.0.0.1\/repo-github\/wordpress-vue-infinite-loading\/publication\/sample-publication-12\/",
"date": "27 Aug 2018",
"taxonomy": [{
"term_id": 37,
"name": "The Project Syndicate",
"slug": "project-syndicate",
"term_group": 0,
"term_taxonomy_id": 37,
"taxonomy": "publication-source",
"description": "",
"parent": 0,
"count": 5,
"filter": "raw",
"term_order": "0"
}, {
"term_id": 38,
"name": "The Star Online",
"slug": "star-online",
"term_group": 0,
"term_taxonomy_id": 38,
"taxonomy": "publication-source",
"description": "",
"parent": 0,
"count": 5,
"filter": "raw",
"term_order": "0"
}]
}, {
"id": "post322",
"slug": "sample-publication-11",
"title": "Sample Publication 11",
"url": "http:\/\/127.0.0.1\/repo-github\/wordpress-vue-infinite-loading\/publication\/sample-publication-11\/",
"date": "27 Aug 2018",
"taxonomy": [{
"term_id": 37,
"name": "The Project Syndicate",
"slug": "project-syndicate",
"term_group": 0,
"term_taxonomy_id": 37,
"taxonomy": "publication-source",
"description": "",
"parent": 0,
"count": 5,
"filter": "raw",
"term_order": "0"
}]
}, {
"id": "post320",
"slug": "sample-publication-10",
"title": "Sample Publication 10 Something Amazing",
"url": "http:\/\/127.0.0.1\/repo-github\/wordpress-vue-infinite-loading\/publication\/sample-publication-10\/",
"date": "21 Aug 2018",
"taxonomy": [{
"term_id": 37,
"name": "The Project Syndicate",
"slug": "project-syndicate",
"term_group": 0,
"term_taxonomy_id": 37,
"taxonomy": "publication-source",
"description": "",
"parent": 0,
"count": 5,
"filter": "raw",
"term_order": "0"
}]
}, {
"id": "post318",
"slug": "sample-publication-9",
"title": "Sample Publication 9",
"url": "http:\/\/127.0.0.1\/repo-github\/wordpress-vue-infinite-loading\/publication\/sample-publication-9\/",
"date": "21 Aug 2018",
"taxonomy": [{
"term_id": 37,
"name": "The Project Syndicate",
"slug": "project-syndicate",
"term_group": 0,
"term_taxonomy_id": 37,
"taxonomy": "publication-source",
"description": "",
"parent": 0,
"count": 5,
"filter": "raw",
"term_order": "0"
}]
}]
Vue
After making sure the endpoint is working, now on the JavaScript side, this is what I have in the simplified version:
// Import node modules.
import 'babel-polyfill'
import DocReady from 'es6-docready'
import $ from 'jquery'
import 'jquery-ui-bundle'
import Foundation from 'foundation-sites'
import Vue from 'vue/dist/vue.js'
import autosize from 'autosize'
import AOS from 'aos'
import axios from 'axios'
// Must wait until DOM is ready before initiating the modules.
DocReady(async () => {
// Render template with Vue.
// Render template with Vue.
// Get json of posts.
var element = document.getElementById('api-posts')
if (element !== null) {
var posts = new Vue({
el: '#api-posts',
data: {
items: [],
counter: 1,
loading: true,
bottom: false,
filter: '',
searchTarget: null,
finish: false,
reset: false,
total: 0,
},
created() {
// https://scotch.io/tutorials/simple-asynchronous-infinite-scroll-with-vue-watchershn
window.addEventListener('scroll', () => {
this.bottom = this.bottomVisible()
})
},
mounted() {
// Check if any filter is on.
var elem = $('#api-posts')
this.filter = elem.data('filter')
// Fetch the first badge.
if (this.filter === 'category') {
this.fetchCatPosts()
} else {
this.fetchPosts()
}
AOS.init({
duration: 1200,
})
},
watch: {
bottom: function (bottom) {
if (this.finish === true) {
return
}
if (bottom) {
this.loading = true
if (this.filter === 'category') {
//...
} else {
this.fetchPosts()
}
}
},
},
methods: {
fetchPosts: async function (event) {
var url = null
// Vanilla JS
// var buttonLoad = document.getElementById('button-load')
// var endpoint = buttonLoad.getAttribute('data-posts-endpoint')
// jQuery
var buttonLoad = $('#button-load-posts')
var endpoint = buttonLoad.data('posts-endpoint')
// With axion - no callbacks.
var getData = await axios.get(endpoint + this.counter)
var data = getData.data
this.counter += 1
// Disable the button if no more data.
// https://www.w3schools.com/jsref/prop_pushbutton_disabled.asp
if (data == null) {
// Does not work for some reasons.
// buttonLoad.disabled = true
// Use jQuery instead.
buttonLoad.addClass('disabled')
this.finish = true
this.loading = false
return
}
// `this` inside methods points to the Vue instance
var self = this
data.map(function(item) {
self.items.push(item)
if (item.total) {
self.total = item.total
}
})
this.loading = false
this.reset = false
},
bottomVisible() {
// https://stackoverflow.com/questions/15615552/get-div-height-with-plain-javascript/15615701
var footerHeight = document.getElementById('footer').clientHeight;
var scrollY = window.scrollY
var visible = document.documentElement.clientHeight
var pageHeight = document.documentElement.scrollHeight - (footerHeight / 2)
var bottomOfPage = visible + scrollY >= pageHeight
return bottomOfPage || pageHeight < visible
},
}
})
}
})
bottomVisible()
is the function to check if the page is being scrolled to the bottom page. If it returns true
, then call fetchPosts
again for the second load/ page of your posts.
And then below is what I have in the PHP file in my custom theme for rendering the data above:
<?php
// Get requested language.
$lang = get_query_var('lang');
$langForApi = $lang ? $lang : 'en';
// JSON API endpoint.
$endpoint = site_url() . '/wp-json/publications/v1/lang/' . $langForApi . '/parent/' . $post->ID .'/page/';
?>
<a href="#" id="button-load-posts" class="button button-load hide" data-posts-endpoint="<?php echo $endpoint; ?>" v-on:click.prevent="fetchPosts"><i class="material-icons">add</i><span>Load More</span></a>
<div id="api-posts" class="row row-list" data-filter="">
<template v-if="items">
<!-- use div with vue else or there is a problem - dunno why! -->
<div>
<!-- publications -->
<div class="container-publications">
<!-- vue - loop -->
<!-- https://laracasts.com/discuss/channels/vue/use-v-for-without-wrapping-element -->
<template v-for="item in items">
<div class="published-item">
<h5 class="heading-published"><a :href="item.url" v-html="item.title"></a></h5>
<div class="published-publishers">
<template v-for="tax in item.taxonomy">
<span class="published-publisher" v-html="tax.name"></span>
</template>
</div>
<span class="published-date">{{ item.date }}</span>
</div>
</template>
<!-- vue - loop -->
</div>
<!-- publications -->
</div>
<!-- use div with vue else or there is a problem - dunno why! -->
<div class="container-spinner">
<div v-if="loading === true" class="sk-circle">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>
<div class="sk-circle4 sk-child"></div>
<div class="sk-circle5 sk-child"></div>
<div class="sk-circle6 sk-child"></div>
<div class="sk-circle7 sk-child"></div>
<div class="sk-circle8 sk-child"></div>
<div class="sk-circle9 sk-child"></div>
<div class="sk-circle10 sk-child"></div>
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
</div>
</template>
</div>
We should get this layout on the browser screen:
Conclusion
You can download the entire source code above here, install it and run it from your local machine and see how it works.
Hope this example is helpful enough if you looking for a solution of infinite scroll. Let me know what you think. Any suggestions and insights, please leave a comment below.