Codementor Events

Creating an infinite scroll with Vue for WordPress

Published Sep 28, 2018
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:

infinite-scroll.png

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.

Discover and read more posts from LAU TIAM KOK
get started
post commentsBe the first to share your opinion
Show more replies