Creating a custom taxonomy filter

Code

June 10, 2022

You can create custom filters for your layouts using Twig templates without the need for additional plugins. The fun part is that you can tweak them to your own needs and reuse them on other projects if needed.

Let’s start by adding a filter that allows us to select a taxonomy and filter our posts based on that.

Creating “components” for reuse

In other frameworks, there are reusable components, bits of code that can be used over and over again but have their own settings and markup. We can do similar using macros in Twig.  For this code tutorial we will be using two “components”, one for the Taxonomy Filter and one for the Pagination.

Taxonomy Filter component

Let’s start with the Taxonomy Filter:

{#
Title:				Taxonomy Filter
Description:			Adds a select form element with a taxonomy terms as options
TemplateType:			component
ComponentName:			TaxFilter
Keywords:
#}
{#
	Usage:
    
    TaxFilter._render( {
            request : request,
            taxonomy: 'category',
            hide_empty: false,
            placeholder: true,
            placeholder_text: '-- select category --',
            id: 'filter-category',
            default : null,
    }) 

#}
{% macro _render( settings ) %}
    {# get the terms #}
    {% 
        set terms = tb.get_terms( { 
            taxonomy: settings.taxonomy,
            hide_empty: settings.hide_empty,
        }) 
    %}
    <select id="{{ settings.id|default( "filter-#{settings.taxonomy}" )}}" name="_{{settings.taxonomy}}">
        {{ settings.placeholder is same as(true) ? _self.placeholder( settings ) }}
        {% for term in terms %}
        {{ _self.term( term , settings ) }}
        {% endfor %}
    </select>
{% endmacro %}

{% macro placeholder( settings ) %}
        <option value=""{{ 
            attribute( settings.request.get,  "_#{settings.taxonomy}" ) == "" 
                ? " SELECTED" 
            }}>
            {{ settings.placeholder_text|default( "-- select #{settings.taxonomy} --" ) }}
        </option>
{% endmacro %}



{% macro term( item , settings  ) %}
<option value="{{item.slug}}"{{ 
    attribute( settings.request.get ,  "_#{item.taxonomy}" ) == item.slug 
    or ( attribute( settings.request.get ,  "_#{item.taxonomy}" ) == null and settings.default|default(null) == item.slug )
        ? " SELECTED" 
        }}>{{ item.name }}</option>
{% endmacro %}
Expand

Copy the code that you see above to the clipboard. Head over to the dashboard and create a new Twig Template. Give it a title of “Taxonomy Filter Component”, paste the code into the code content area and save it. Make sure the check if the Twig Template slug is as expected, it should be ‘taxonomy-filter-component‘.

Pagination component

Let’s also add a Pagination component:

{#
Title:				On Page Pagination
Description:			Pagination using same page and _page request parameter
TemplateType:			component
ComponentName:			Pagination
Keywords:
#}
{% macro _render(posts) %}
	{% if posts.pagination and posts.pagination.total > 1 %}
		<ul class="uk-pagination uk-flex-center uk-margin-medium-top" uk-margin>
		{% if posts.pagination.current == 1 %}
			<li></li>
		{% else %}
			<li><a href="{{_self.pagelink(tb.intval(posts.pagination.current) - 1)}}"><span uk-pagination-previous></span></a></li>
		{% endif %}
		{% for page in posts.pagination.pages %}
			<li class="{{page.title==posts.pagination.current ? 'current' }}"><a href="{{_self.pagelink(page.title)}}">{{page.title}}</a></li>
		{% endfor %}
		{% if posts.pagination.current == posts.pagination.total %}
			<li></li>
		{% else %}
			<li><a href="{{_self.pagelink(tb.intval(posts.pagination.current) + 1)}}"><span uk-pagination-next></span></a></li>
		{% endif %}
		</ul>
	{% endif %}
{% endmacro %}

{% macro pagelink(page) %}
  {% set current_url = fn('\\Timber\\URLHelper::get_current_url' , '' ) %}
  {# remove _page / paging #}
  {% set current_url = tb.preg_replace( '/(?:\\?_page|&_page)=[0-9]{1,}/' , '', current_url ) %}
  {% if current_url|slice(current_url|length-1,1) == '?' %}
    {{- "#{current_url}_page=#{page}" -}}
  {% elseif '?' in current_url  %}
    {{- "#{current_url}&_page=#{page}" -}}
  {% else %}
    {{- "#{current_url}?_page=#{page}" -}}
  {% endif %}
{% endmacro %}
Expand

Copy this code to the clipboard. Head over to the dashboard and create a new Twig Template. Give it a title of “On Page Pagination Component”, paste the code into the code content area and save it. Make sure the check if the Twig Template slug is as expected, it should be ‘on-page-pagination-component‘.

Now that we have both our components saved, we can use them in a Beaver Builder Timber Posts Module. Drag one into your layout and add the following Twig code:

{%- import 'taxonomy-filter-component.twig' as TaxFilter -%}
<form id="taxfilters">
{{ 
TaxFilter._render( {
        request : request,
        taxonomy: 'category',
        hide_empty: false,
        placeholder: true,
        placeholder_text: '-- select category --',
        id: 'filter-category',
        default : null,
    }) 
}}
</form>

As the code suggests, this imports the ‘component’ as a new variable TaxFilter. We wrap our select input in a form tag with a unique id. The main macro for the component is called ‘_render’, so that we only need to use TaxFilter._render() to output our component with our settings applied.

Please not that the first two parameters are required:

  • The request param is always request (no quotes!) and it NEEDS to be provided
  • taxonomy is the slug of the taxonomy terms to display

Adding a module to render our filtered posts archive

Before moving on to the javascript that powers the browser update, we will first add a Timber Posts Module to render the filtered posts. Drag a Timber Posts Module into your layout, right below the filters.

Add the following code to the template:

{%- import 'on-page-pagination-component.twig' as Pagination -%}
{# set basic arguments #}
{% 
    set args = {
        post_type : 'post',
        posts_per_page : 10,
    } 
%}
{% 
    set tax_query = {} 
%}
{# if a request variable _category is found, merge with tax_query #}
{# 
    in case of multiple filters, repeat if - endif block 
    and edit contents to match your taxonomy 
#}
{% if request.get._category %}
    {% set 
            tax_query = tax_query|merge( {
                category_clause : {
                    taxonomy : 'category',
                    field : 'slug',
                    terms : [ request.get._category ],
                    include_children : true,
                    operator : 'IN',
                }
            } )
    %}
{% endif %}
{# check if _page request variable is found, merge into args to get next set of posts #}
{% if request.get._page %}
    {% set args = args|merge( { paged : tb.intval( request.get._page ) } ) %}
{% endif %}
{# if tax_query has entries, merge with args #}
{% if tax_query|length %}
    {% set args = args|merge( { tax_query: tax_query|merge({ relation: 'AND' }) } ) %}
{% endif %}
{# get the posts #}
{% set posts = PostQuery( args ) %}
{% for item in posts %}
<p><a href="{{ item.link}}">{{ item.title }}</a></p>
{% endfor %}
{# render the pagination for these posts #}
{{ Pagination.render(posts) }}
Expand

Adding JavaScript to automatically reload the page while switching filters

The last step in our filter is adding some JavaScript that will trigger once we change (one of) our filter select(s).

When a select changes in value, it will try to get all select names and values for the form it belongs to, and update the current url with these filter settings.

You can copy the javascript code below and paste it into the “Layout CSS & Javascript” (CTRL/CMD + Y) window on the JavaScript tab.

var filterHelpers = {
	/**
	 * [qToArr description]
	 * @param  {[type]} q [description]
	 * @return {[type]}   [description]
	 * @url http://jsfiddle.net/remus/eSL2c/
	 * filterHelpers.qToArr(location.search.substring(1));
	 */
	qToArr : function(q) {
	    q = decodeURIComponent((q).replace(/\+/g, '%20'));
	    arr = {};
	    var a = q.split(/&(?!amp;)/g);
	    for (let x in a)
	    {
	        var pair = a[x].split('=');
	        arr[pair[0]] = pair[1];
	    }
	    return arr;
	},
	
	/**
	 * Collect the values for dropdown fields in formid
	 * @param  {[type]} formid [description]
	 * @return {[type]}        [description]
	 */
	collectFilterParams: function( formid ) {
		var searchparameters = [];
		jQuery( '#' + formid + ' [id^="filter-"]').each( function () {
			var name = jQuery( this ).attr( 'name' );
            searchparameters[name] = jQuery(this).val();
		});

		return searchparameters;
	},
	
	updatePushURL: function( formid ) {
	  	if ( history.pushState ) {
			let searchparams = this.collectFilterParams( formid );
			let compoundstring = [];

			for ( let keyname of Object.keys( searchparams ) ) {
				if (keyname == 'action') continue;
				compoundstring.push( keyname + '=' + searchparams[ keyname ] );
			}

			let compound  = (compoundstring.length === 0) ? '' : '?' + compoundstring.join( '&' );

	    	var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + compound ;

	    	window.history.pushState({path:newurl},'',newurl);
	    	
	    	return newurl;
	  	}
	  	return false;
	},	
    
};

(function($){
    
    $(document).ready(function() {
        $( '[id^="filter-"]' ).on('change', function() {
            // find the form this control belongs to and collect the values
            $form = $(this).closest( 'form' );
            const newurl = filterHelpers.updatePushURL( $form.attr( 'id' ) );
            if (newurl) window.location = newurl;

        });
    });
    
})(jQuery);
Expand

Beaverplugins

Web ninja with PHP/CSS/JS and Wordpress skills. Also stand-in server administrator, father of four kids and husband to a beautiful wife.
Always spends too much time figuring out ways to do simple things even quicker. So that you can benefit.