Suppose you have a calendar loaded with events that span all hours of the day. You can already filter by a pretty general time, as seen in the screenshot below:

But what if you want to allow for more granular filtering, perhaps by a specific time? We’ll show you how in this tutorial.

Getting started

💡 Make sure you have a backup of your site files and database.

I’ve created a tec-fbar-custom-filter directory in my WordPress plugins directory. In my development installation the resulting path, relative to WP root directory, is wp-content/plugins/tec-fbar-custom-filter. Yours might not match this path exactly depending on your setup. 

The easiest way to create a new filter is to modify an existing filter.
You can find the filters in your plugins directory at: /events-filterbar/src/Tribe/Filters/

The code I’ll show in this tutorial will live in a dedicated plugin.

You can also use this handy extension to get you started.

Since we’ll be adding a custom filter that is similar to the existing Time of Day filter, I’m going to grab that one.

To keep the code organized I’ve created a main plugin file, tribe-ext-custom-filter.php, and a file that will hold my custom filter class, Time_Of_Day_Custom_Filter.php.

My plugin directory looks like this:

        tec-fbar-custom-filter
        └── tribe-ext-custom-filter.php
        ├── src
                └ Time_Of_Day_Custom_Filter.php

In the Time_Of_Day_Custom_Filter.php file I’ve called my filter class Time_Of_Day_Custom_Filter. (Keeping all the names the same helps keep them organized in my head.)

I’ll need to decide what type of filtering I want to display. The checkbox filter is a good fit for this, so I’ll go ahead and keep that in place.

So far, my class looks like this:

<?php

/**
 * Customized version of the Time of Day filter that allows selecting by specific hour ranges in the afternoon.
 *
 * New filter available in WP-Admin > Events > Settings > Filters
 */

/**
 * Class Time_Of_Day_Custom_Filter
 */
class Time_Of_Day_Custom_Filter extends \Tribe__Events__Filterbar__Filter {
	/**
	 * The type of this filter.
	 *
	 * Available options are 'select', 'multiselect', 'checkbox', 'radio', and 'range'.
	 *
	 * @var string
	 */
	public $type = 'checkbox';

}

The next part is where things get interesting. We need to change both the get_values() function, and the setup_join_clause() and setup_where_clause() functions in order for our new filters to work.

Changing the array values

The current code allows us to filter in six-hour blocks. We’ll modify that to allow filtering by the hour.

In the function get_values(), change the time of day array to look like this:

/**
 * Returns the available values this filter will display on the front end, when the user clicks on it.
 *
 * @return array <string,string> A map of the available values relating values to their human-readable format.
 */
protected function get_values() {
        // The time-of-day filter.
        $time_of_day_array = [
                'allday' => __( 'All Day', 'tribe-events-filter-view' ),
                '12-13' => __( '12:00 - 1:00 pm', 'tribe-events-filter-view' ),
                '13-14' => __( '1:00 - 2:00 pm', 'tribe-events-filter-view' ),
                '14-15' => __( '2:00 - 3:00 pm', 'tribe-events-filter-view' ),
                '15-16' => __( '3:00 - 4:00 pm', 'tribe-events-filter-view' ),
                '16-17' => __( '4:00 - 5:00 pm', 'tribe-events-filter-view' ),
                '17-18' => __( '5:00 - 6:00 pm', 'tribe-events-filter-view' ),
                '18-19' => __( '6:00 - 7:00 pm', 'tribe-events-filter-view' ),
                '19-20' => __( '7:00 - 8:00 pm', 'tribe-events-filter-view' ),
                '20-21' => __( '8:00 - 9:00 pm', 'tribe-events-filter-view' ),
                '21-22' => __( '9:00 - 10:00 pm', 'tribe-events-filter-view' ),
                '22-23' => __( '10:00 - 11:00 pm', 'tribe-events-filter-view' ),
                '23-24' => __( '11:00 - 12:00 pm', 'tribe-events-filter-view' ),
        ];

        $time_of_day_values = [];

        foreach ( $time_of_day_array as $value => $name ) {
                $time_of_day_values[] = [
                        'name' => $name,
                        'value' => $value,
                ];
        }

        return $time_of_day_values;
}

The rest of the get_values() function will stay the same.

Changing the variable names

The next methods I’ll look at are the setup_where_clause() and setup_join_clause() ones . I’m not making any radical change to the logic, but I’m trimming down the code to fit my custom filter need.

The new code for the setup_join_clause method looks like this:

// These are needed to make the join aliases unique
protected $alias = '';
protected $tod_start_alias = '';
protected $tod_duration_alias = '';

/**
 * Sets up the filter JOIN clause.
 *
 * This will be added to the running events query to add (JOIN) the tables the filter requires.
 *
 * Specifically, this filter will JOIN on the post meta table to use the event start time and all-day information.
 */
protected function setup_join_clause() {
	if ( function_exists( 'posts_join' ) ) {
			add_filter( 'posts_join', array( 'Tribe__Events__Query', 'posts_join' ), 10, 2 );
	}

	global $wpdb;
	$values = $this->currentValue;

	$all_day_index = array_search( 'allday', $values );
	if ( false !== $all_day_index ) {
		unset( $values[ $all_day_index ] );
	}

	$this->alias = 'custom_all_day_' . uniqid();
	$this->tod_start_alias = 'tod_start_date_' . uniqid();
	$this->tod_duration_alias = 'tod_duration_' . uniqid();

	$joinType = empty( $all_day_index ) ? 'LEFT' : 'INNER';

	$this->joinClause .= " {$joinType} JOIN {$wpdb->postmeta} AS {$this->alias} ON ({$wpdb->posts}.ID = {$this->alias}.post_id AND {$this->alias}.meta_key = '_EventAllDay')";

	if ( ! empty( $values ) ) { // values other than allday
		$this->joinClause .= " INNER JOIN {$wpdb->postmeta} AS {$this->tod_start_alias} ON ({$wpdb->posts}.ID = {$this->tod_start_alias}.post_id AND {$this->tod_start_alias}.meta_key = '_EventStartDate')";
		$this->joinClause .= " INNER JOIN {$wpdb->postmeta} AS {$this->tod_duration_alias} ON ({$wpdb->posts}.ID = {$this->tod_duration_alias}.post_id AND {$this->tod_duration_alias}.meta_key = '_EventDuration')";
	}
}

The code of the setup_where_clause() method looks like this:

/**
 * Sets up the filter WHERE clause.
 *
 * This will be added to the running events query to apply the matching criteria, time-of-day, handled by the
 * custom filter.
 *
 * @throws Exception
 */
 protected function setup_where_clause() {
	global $wpdb;
	$clauses = [];

	if ( in_array( 'allday', $this->currentValue, true ) ) {
		$clauses[] = "( {$this->alias}.meta_value = 'yes' )";
	} else {
		$this->whereClause = " AND ( {$this->alias}.meta_id IS NULL ) ";
	}

	foreach ( $this->currentValue as $value ) {
		if ( 'allday' === $value ) {
			// Handled earlier.
			continue;
		}

		list( $start_hour, $end_hour ) = explode( '-', $value );
		$start          = $start_hour . ':00:00';
		$end              = $end_hour . ':00:00';
		$clauses[] = $wpdb->prepare(
			"( TIME( CAST( {$this->tod_start_alias}.meta_value as DATETIME ) ) >= %s
			AND TIME( CAST({$this->tod_start_alias}.meta_value as DATETIME)) < %s )",
			$start,
			$end
		);
	}

	$clauses = implode( ' OR ', $clauses );

	$this->whereClause .= " AND ({$clauses})";
}

To make it work with the previous version of the View architecture, the last thing I’ll need to do is to instantiate the custom filter class when Filter Bar initializes: I’m doing this in the tribe-ext-custom-filter.php file:

<?php
/**
 * Plugin Name:              Filter Bar Extension: Custom Time of Day Filter
 *
 * Description:              Create a custom filter for Filter Bar that filters displayed events down by time of day.
 */

/**
 * Includes the custom filter class and creates an instance of it.
 */
function tec_kb_create_filter() {
	if ( ! class_exists( 'Tribe__Events__Filterbar__Filter' ) ) {
		return;
	}

	include_once __DIR__ . '/src/Time_Of_Day_Custom_Filter.php';

	new \Time_Of_Day_Custom_Filter(
	__( 'Time Custom', 'tribe-events-filter-view' ),
	'filterbar_time_of_day_custom'
	);
}

Making it work in calendar views

First things first, in the tribe-ext-custom-filter.php file, add my custom filter to the ones handled by the calendar views available with Filter Bar. Here’s the updated code:

<?php
/**
 * Filters the map of filters available on the front-end to include the custom one.
 *
 * @param array<string,string> $map A map relating the filter slugs to their respective classes.
 *
 * @return array<string,string> The filtered slug to filter class map.
 */
function tec_kb_filter_map( array $map ) {
	if ( ! class_exists( 'Tribe__Events__Filterbar__Filter' ) ) {
		// This would not make much sense, but let's be cautious.
		return $map;
	}

	// Include the filter class.
	include_once __DIR__ . '/src/Time_Of_Day_Custom_Filter.php';

	// Add the filter class to our filters map.
	$map['filterbar_time_of_day_custom'] = 'Time_Of_Day_Custom_Filter';

	// Return the modified $map.
	return $map;
}

/**
 * Filters the Context locations to let the Context know how to fetch the value of the filter from a request.
 *
 * Here we add the `time_of_day_custom` as a read-only Context location: we'll not need to write it.
 *
 * @param array<string,array> $locations A map of the locations the Context supports and is able to read from and write
 *                                                                              to.
 *
 * @return array<string,array> The filtered map of Context locations, with the one required from the filter added to it.
 */
function tec_kb_filter_context_locations( array $locations ) {
	// Read the filter selected values, if any, from the URL request vars.
   $locations['filterbar_time_of_day_custom'] = [ 
				'read' => [ 
					\Tribe__Context::QUERY_VAR   => [ 'tribe_filterbar_time_of_day_custom' ],
					\Tribe__Context::REQUEST_VAR => [ 'tribe_filterbar_time_of_day_custom' ]
				], 
			];

	// Return the modified $locations.
	return $locations;
}

// Make it work with calendar views.
add_filter( 'tribe_context_locations', 'tec_kb_filter_context_locations' );
add_filter( 'tribe_events_filter_bar_context_to_filter_map', 'tec_kb_filter_map' );

Read the inline docs to understand what we’re doing line-by-line.

Or skip to the bottom of the article to get the full plugin code in its final version.

Finally, to wrap up calendar views support, I need to use the Tribe\Events\Filterbar\Views\V2\Filters\Context_Filter in the custom filter class code. This will automagically take care of a number of issues and make the class, without further changes, compatible with views (in the screenshot, I’m showing only the class start, as there’s really nothing else changed):

<?php

/**
 * Customized version of the Time of Day filter that allows selecting by specific hour ranges in the afternoon.
 *
 * New filter available in WP-Admin > Events > Settings > Filters
 */

use Tribe\Events\Filterbar\Views\V2\Filters\Context_Filter;

/**
 * Class Time_Of_Day_Custom_Filter
 */
class Time_Of_Day_Custom_Filter extends \Tribe__Events__Filterbar__Filter {
	// Use the trait required for filters to correctly work with Views V2 code.
	use Context_Filter;

Wrapping up, the code

Our final tribe-ext-custom-filter.php code looks like this (If you download the extension, there is more code in there, but this is the part that matters for this tutorial):

<?php
/**
 * Plugin Name:              Filter Bar Extension: Custom Time of Day Filter
 * Description:              Create a custom filter for Filter Bar that filters displayed events down by time of day.
 */

/**
 * Filters the map of filters available on the front-end to include the custom one.
 *
 * @param array<string,string> $map A map relating the filter slugs to their respective classes.
 *
 * @return array<string,string> The filtered slug to filter class map.
 */
function tec_kb_filter_map( array $map ) {
	if ( ! class_exists( 'Tribe__Events__Filterbar__Filter' ) ) {
	// This would not make much sense, but let's be cautious.
	return $map;
	}

	// Include the filter class.
	include_once __DIR__ . '/src/Time_Of_Day_Custom_Filter.php';

	// Add the filter class to our filters map.
	$map['filterbar_time_of_day_custom'] = 'Time_Of_Day_Custom_Filter';

	// Return the modified $map.
	return $map;
}

/**
 * Filters the Context locations to let the Context know how to fetch the value of the filter from a request.
 *
 * Here we add the `time_of_day_custom` as a read-only Context location: we'll not need to write it.
 *
 * @param array<string,array> $locations A map of the locations the Context supports and is able to read from and write
 *                                                                              to.
 *
 * @return array<string,array> The filtered map of Context locations, with the one required from the filter added to it.
 */
function tec_kb_filter_context_locations( array $locations ) {
	// Read the filter selected values, if any, from the URL request vars.
    $locations['filterbar_time_of_day_custom'] = [ 
				'read' => [ 
					\Tribe__Context::QUERY_VAR   => [ 'tribe_filterbar_time_of_day_custom' ],
					\Tribe__Context::REQUEST_VAR => [ 'tribe_filterbar_time_of_day_custom' ]
				], 
			];

	// Return the modified $locations.
	return $locations;
}

/**
 * Includes the custom filter class and creates an instance of it.
 */
function tec_kb_create_filter() {
	if ( ! class_exists( 'Tribe__Events__Filterbar__Filter' ) ) {
		return;
	}

	include_once __DIR__ . '/src/Time_Of_Day_Custom_Filter.php';

	new \Time_Of_Day_Custom_Filter(
	__( 'Time Custom', 'tribe-events-filter-view' ),
	'filterbar_time_of_day_custom'
	);
}



// Make it work with calendar views.
add_filter( 'tribe_context_locations', 'tec_kb_filter_context_locations' );
add_filter( 'tribe_events_filter_bar_context_to_filter_map', 'tec_kb_filter_map' );

While the Time_Of_Day_Custom_Filter.php custom filter code is this:

<?php

/**
 * Customized version of the Time of Day filter that allows selecting by specific hour ranges in the afternoon.
 *
 * New filter available in WP-Admin > Events > Settings > Filters
 */

use Tribe\Events\Filterbar\Views\V2\Filters\Context_Filter;

/**
 * Class Time_Of_Day_Custom_Filter
 */
class Time_Of_Day_Custom_Filter extends \Tribe__Events__Filterbar__Filter {
	// Use the trait required for filters to correctly work with Views V2 code.
	use Context_Filter;

	/**
	 * The type of this filter.
	 *
	 * Available options are 'select', 'multiselect', 'checkbox', 'radio', and 'range'.
	 *
	 * @var string
	 */
	public $type = 'checkbox';


	// These are needed to make the join aliases unique
	protected $alias = '';
	protected $tod_start_alias = '';
	protected $tod_duration_alias = '';

	/**
	 * Returns the available values this filter will display on the front end, when the user clicks on it.
	 *
	 * @return array <string,string> A map of the available values relating values to their human-readable format.
	 */
	protected function get_values() {
		// The time-of-day filter.
		$time_of_day_array = [
			'allday' => __( 'All Day', 'tribe-events-filter-view' ),
			'12-13' => __( '12:00 - 1:00 pm', 'tribe-events-filter-view' ),
			'13-14' => __( '1:00 - 2:00 pm', 'tribe-events-filter-view' ),
			'14-15' => __( '2:00 - 3:00 pm', 'tribe-events-filter-view' ),
			'15-16' => __( '3:00 - 4:00 pm', 'tribe-events-filter-view' ),
			'16-17' => __( '4:00 - 5:00 pm', 'tribe-events-filter-view' ),
			'17-18' => __( '5:00 - 6:00 pm', 'tribe-events-filter-view' ),
			'18-19' => __( '6:00 - 7:00 pm', 'tribe-events-filter-view' ),
			'19-20' => __( '7:00 - 8:00 pm', 'tribe-events-filter-view' ),
			'20-21' => __( '8:00 - 9:00 pm', 'tribe-events-filter-view' ),
			'21-22' => __( '9:00 - 10:00 pm', 'tribe-events-filter-view' ),
			'22-23' => __( '10:00 - 11:00 pm', 'tribe-events-filter-view' ),
			'23-24' => __( '11:00 - 12:00 pm', 'tribe-events-filter-view' ),
		];

		$time_of_day_values = [];

		foreach ( $time_of_day_array as $value => $name ) {
			$time_of_day_values[] = [
				'name' => $name,
				'value' => $value,
			];
		}

		return $time_of_day_values;
	}

	/**
	 * Sets up the filter JOIN clause.
	 *
	 * This will be added to the running events query to add (JOIN) the tables the filter requires.
	 *
	 * Specifically, this filter will JOIN on the post meta table to use the event start time and all-day information.
	 */
	protected function setup_join_clause() {
		if ( function_exists( 'posts_join' ) ) {
			add_filter( 'posts_join', array( 'Tribe__Events__Query', 'posts_join' ), 10, 2 );
		}

		global $wpdb;
		$values = $this->currentValue;

		$all_day_index = array_search( 'allday', $values );
		if ( false !== $all_day_index ) {
			unset( $values[ $all_day_index ] );
		}

		$this->alias = 'custom_all_day_' . uniqid();
		$this->tod_start_alias = 'tod_start_date_' . uniqid();
		$this->tod_duration_alias = 'tod_duration_' . uniqid();

		$joinType = empty( $all_day_index ) ? 'LEFT' : 'INNER';

		$this->joinClause .= " {$joinType} JOIN {$wpdb->postmeta} AS {$this->alias} ON ({$wpdb->posts}.ID = {$this->alias}.post_id AND {$this->alias}.meta_key = '_EventAllDay')";

		if ( ! empty( $values ) ) { // values other than allday
			$this->joinClause .= " INNER JOIN {$wpdb->postmeta} AS {$this->tod_start_alias} ON ({$wpdb->posts}.ID = {$this->tod_start_alias}.post_id AND {$this->tod_start_alias}.meta_key = '_EventStartDate')";
			$this->joinClause .= " INNER JOIN {$wpdb->postmeta} AS {$this->tod_duration_alias} ON ({$wpdb->posts}.ID = {$this->tod_duration_alias}.post_id AND {$this->tod_duration_alias}.meta_key = '_EventDuration')";
		}
	}

	/**
	 * Sets up the filter WHERE clause.
	 *
	 * This will be added to the running events query to apply the matching criteria, time-of-day, handled by the
	 * custom filter.
	 *
	 * @throws Exception
	 */
	protected function setup_where_clause() {
		global $wpdb;
		$clauses = [];

		if ( in_array( 'allday', $this->currentValue, true ) ) {
			$clauses[] = "( {$this->alias}.meta_value = 'yes' )";
		} else {
			$this->whereClause = " AND ( {$this->alias}.meta_id IS NULL ) ";
		}

		foreach ( $this->currentValue as $value ) {
			if ( 'allday' === $value ) {
				// Handled earlier.
				continue;
			}

			list( $start_hour, $end_hour ) = explode( '-', $value );
			$start          = $start_hour . ':00:00';
			$end              = $end_hour . ':00:00';
			$clauses[] = $wpdb->prepare(
				"( TIME( CAST( {$this->tod_start_alias}.meta_value as DATETIME ) ) >= %s
				AND TIME( CAST({$this->tod_start_alias}.meta_value as DATETIME)) < %s )",
				$start,
				$end
			);
		}

		$clauses = implode( ' OR ', $clauses );

		$this->whereClause .= " AND ({$clauses})";
	}
}

Once you have your code in place, you’ll need to adjust your settings in Events → Settings → Filters so that your new filter will display on the front end.

Once you have that done, you should have the new filters in the Filter Bar, like so:

Final thoughts

Now that you’ve seen how easy it is to modify and extend the existing filters, hopefully, this will spark some ideas for you to create your own!

Let us know if you come up with anything cool. We’d love it if you’d share with the community!