Filtering Lists Dynamically With Vue on the Server Side is Easier Than You’d Think

Avatar of Dan Brellis
Dan Brellis on

I recently attended the ARTIFACT conference in Austin, TX, and was inspired by a few talks about accessibility through the lens of site performance. It became clear to me that there is this tendency to rely on big JavaScript frameworks to handle the work — like React, Vue, and Angular — but that can be overkill in some cases. That is, negatively affecting site performance, and thus accessibility. At the same time, these frameworks can make development easier and more efficient for developers. My big takeaway from the conference was to see how a fast, performant experience can be balanced with my own development process.

This was on my mind as I was building a list-filtering feature on a project a few days after the conference. Pretty standard stuff: I needed a list of posts and some category filtering. I was using CraftCMS for front-end routes and templating as well as some Vue components here and there for some added JavaScript juiciness. Not a full-on “single page app” but more like a sprinkle of Vue.

The typical way one might approach this is to:

  1. render the page with an empty div using Craft/Twig
  2. mount a Vue component to that div
  3. make an Ajax call from the Vue component to an API to gather the posts as JSON
  4. render the posts and tie in the filtering.

Since the posts are held as an array within Vue, dynamic list rendering is a pretty cut and dry task.

Simple. Done, right? Well… that extra Ajax request means the user is presented with no content on the initial load depending on the user’s network, which might take some time. We could add a loading indicator, but maybe we can do better?

Preferably, the posts are rendered on the initial page request from the CMS.

But how do we get the static HTML “hooked up” with Vue for the filtering?

After taking a step back to rethink the problem, I realized I could use v-if directives to achieve the same thing with some inline JavaScript from Twig (“in the loop”). Below, I’ll show how I went about it.

My original project used CraftCMS, but I’m going to do the demos below in WordPress. This is just a concept. It can be applied to CraftCMS/Twig or any other CMS/templating engine combo.

First we need a filtering UI. This will likely go above the start of the loop in an archive template.

<?php $terms = get_terms( [
  'taxonomy' => 'post_tag', // I used tags in this example, but any taxonomy would do
  'hide_empty' => true,
  'fields' => 'names'
] );

if(!empty($terms)): ?>
  <div>
    Filter: 
    <ul class="filters">
      <li class="filters__item"><button class="filters__button" :class="{'filters__button--active': tag === ''}" @click="tag = ''">All</button></li>
      <?php foreach($terms as $term): ?>
      <li class="filters__item">
        <button class="filters__button" :class="{'filters__button--active': tag === '<?php echo $term; ?>'}" @click="tag = '<?php echo $term; ?>'"><?php echo $term; ?></button>
      </li>
      <?php endforeach; ?>
    </ul>
    <p aria-live="polite">Showing posts tagged {{ tag ? tag : 'all' }}.</p>
  </div>
<?php endif; ?>

Following along with the code, we get some tags from WordPress with get_terms() and output them in a foreach loop. You’ll notice the button for each tag has some Vue directives we’ll use later.

We have our loop!

    <div class="posts">
      <?php
      // Start the Loop.
      while ( have_posts() ) : the_post();
    
        <article id="post-<?php the_ID(); ?>"
          <?php post_class(); ?>
          v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(),  ['fields' => 'names'])); ?>.includes(tag) || tag === ""'
        >
          <header class="entry-header">
            <h2><?php the_title(); ?></h2>
          </header>
    
          <div class="entry-content">
            <?php the_excerpt(); ?>
          </div>
        </article>
    
      // End the loop.
      endwhile; ?>
    </div>

This is a pretty standard WordPress loop for posts. You’ll notice some Vue directives that make use of PHP outputting CMS content.

Aside from some styling, all that’s left is the Vue “app.” Are you ready for it? Here it is:

new Vue({
  el: '#filterablePosts',
  data: {
    'tag': ''
  }
});

Yes, really, that’s all that’s needed in the JavaScript file to get this to work!

So, what’s going on here?

Well, instead of some JSON array of posts that gets fed into Vue, we output the posts on the initial page load with WordPress. The trick is to use PHP to output what’s needed in the Vue directives: v-if and :class.

What’s happening on the filter buttons is an onclick event handler (@click) that sets the Vue variable “tag” to the value of the WordPress post tag.

@click="tag = '<?php echo $term; ?>'"

Also, if that Vue variable equals the value of the button (in the :class directive), it adds an active class for the button. This is just for styling.

:class="{'filters__button--active': tag === '<?php echo $term; ?>'}"

For the list of articles, we conditionally display them based on the value of the Vue “tag” variable:

v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(),  ['fields' => 'names'])); ?>.includes(tag) || tag === ""'

The PHP function json_encode allows us to output an array of post tags as JavaScript, which means we can use .includes() to see if the Vue “tag” variable is in that array. We also want to show the article if no tag is selected.

Here it is put together using the Twenty Nineteen theme template archive.php as a base:

<?php get_header(); ?>
  <section id="primary" class="content-area">
    <main id="main" class="site-main">
      <?php if ( have_posts() ) : ?>
        <header class="page-header">
          <?php the_archive_title( '<h1 class="page-title">', '</h1>' ); ?>
        </header>

        <div class="postArchive" id="filterablePosts">
          <?php $terms = get_terms( [
              'taxonomy' => 'post_tag',
              'hide_empty' => true,
              'fields' => 'names'
          ] );

          if(!empty($terms)): ?>
            <div class="postArchive__filters">
              Filter: 
              <ul class="postArchive__filterList filters">
                <li class="filters__item"><button class="filters__button" :class="{'filters__button--active': tag === ''}" @click="tag = ''" aria-controls="postArchive__posts">All</button></li>
  
                <?php foreach($terms as $term): ?>
                  <li class="filters__item">
                    <button class="filters__button" :class="{'filters__button--active': tag === '<?php echo $term; ?>'}" @click="tag = '<?php echo $term; ?>'" aria-controls="postArchive__posts"><?php echo $term; ?></button>
                  </li>
                <?php endforeach; ?>
  
              </ul>
  
              <p aria-live="polite">Showing {{ postCount }} posts tagged {{ tag ? tag : 'all' }}.</p>
            </div>
          <?php endif; ?>

            <div class="postArchive__posts">
              <?php
              // Start the Loop.
              while ( have_posts() ) : the_post(); ?>
                <article
                  id="post-<?php the_ID(); ?>"
                  <?php post_class(); ?>
                  v-if='<?php echo json_encode(wp_get_post_tags(get_the_ID(), ['fields' => 'names'])); ?>.includes(tag) || tag === ""'
                >
                  <header class="entry-header">
                    <h2><?php the_title(); ?></h2>
                  </header>
        
                  <div class="entry-content">
                      <?php the_excerpt(); ?>
                  </div>

                </article>
              <?php endwhile; // End the loop. ?>
          </div>
        </div> 
      
      <?php
      // If no content, include the "No posts found" template.
      else :
        get_template_part( 'template-parts/content/content', 'none' );
      endif; ?>
    </main>
  </section>

<?php
get_footer();

Here’s a working example on CodePen

See the Pen
Dynamic List Filtering in Vue using Server-side data fetching
by Dan Brellis (@danbrellis)
on CodePen.

Bonus time!

You may have noticed that I added in an aria-live="polite" notifier below the filter button list to let assistive tech users know the content has changed.

<p aria-live="polite">Showing {{ postCount }} posts tagged {{ tag ? tag : 'all' }}.</p>

To get the postCount Vue variable, we add some extra JavaScript to our Vue component:

new Vue({
  el: '#filterablePosts',
  data: {
    'tag': '',
    'postCount': ''
},
  methods: {
    getCount: function(){
      let posts = this.$el.getElementsByTagName('article');
      return posts.length;
  }
  },
  beforeMount: function(){
    this.postCount = this.getCount();
  },
  updated: function(){
    this.postCount = this.getCount();
  }
});</p>

The new method getCount is used to select the article elements in our component div and return the length. Before the Vue component mounts we get the count to add to our new Vue postCount variable. Then, when the component updates after the user selects a tag, we get the count again and update our variable.