In this post, I am going to share a simple way of using Ajax filter or search for WordPress. I love using Ajax in my projects. It is neat, light and fast. Most importantly the user experience is much better.

The Scenario

We are going to build a search form like the one shown below. This form will search for movies which is a custom post type. We will also filter the result with a few fields – Movie Year, IMDB Rating, Language and Genre.

Check the Final Output

How it Works?

Ajax is very simple but can seem very tricky at first. In our scenario, we are going to send data as a request (search keywords, year, rating, language, genre) to the server and it will do a query with the data then return a callback with the results.

Initial Setup

For the purpose of this project, we may create a fresh WordPress install and populate dummy data for the trial. We will basically edit 2 files in the theme – functions.php and script.js. As always, please make sure you are using a child theme. I used the Twenty Sixteen Theme’s child theme in my demo. I added a custom post type called movie and created the custom fields with Advanced Custom Fields plugin. You can add a custom post type with the following code added to the functions.php file.

<?php 
function register_custom_post_type_movie() {
	$args = array(
		"label" => __( "Movies", "" ),
		"labels" => array(
			"name" => __( "Movies", "" ),
			"singular_name" => __( "Movie", "" ),
			"featured_image" => __( "Movie Poster", "" ),
			"set_featured_image" => __( "Set Movie Poster", "" ),
			"remove_featured_image" => __( "Remove Movie Poster", "" ),
			"use_featured_image" => __( "Use Movie Poster", "" ),
		),
		"public" => true,
		"publicly_queryable" => true,
		"show_ui" => true,
		"show_in_rest" => false,
		"has_archive" => false,
		"show_in_menu" => true,
		"exclude_from_search" => false,
		"capability_type" => "post",
		"map_meta_cap" => true,
		"hierarchical" => false,
		"rewrite" => array( "slug" => "movie", "with_front" => true ),
		"query_var" => true,
		"supports" => array( "title", "editor", "thumbnail" ),
		"taxonomies" => array( "category" ),
	);
	register_post_type( "movie", $args );
}
add_action( 'init', 'register_custom_post_type_movie' );

Alternatively, you can use a plugin like Custom Post Types UI for adding custom post types with a user interface.

Before we create some movie posts, we need to create the custom fields which are used as filters.

The fields are pretty self-explanatory but if you are confused, here is the exported JSON file of the field group which you can download, unzip and import on ACF.

Download acf-export-2018-04-02.zip

Once you have the custom fields ready, you can start posting some movie posts. The add new movie post page should look something like the following.

You can see, I added some categories. They will work as the genre of the movie. After creating some demo movie posts, we are ready to start coding.

Creating a Shortcode for the Fitler/Search Form

In this step, we are creating a shortcode that will return the form code. So, we can use the shortcode in any part of the website to use the form. Put the following code on functions.php file.

<?php
// Shortcode: [my_ajax_filter_search]
function my_ajax_filter_search_shortcode() {
    ob_start(); ?>

	Test Shortcode Output
	<!-- FORM CODE WILL GOES HERE -->
	
	<?php
    return ob_get_clean();
}

add_shortcode ('my_ajax_filter_search', 'my_ajax_filter_search_shortcode');

Now use the shortcode [my_ajax_filter_search] in any page content and you should see “Test Shortcode Output” as output. As our shortcode is working fine, we are going to put actual form code that we will use the filter/search form.

<?php
// Shortcode: [my_ajax_filter_search]
function my_ajax_filter_search_shortcode() {
    ob_start(); ?>

	<div id="my-ajax-filter-search">
        <form action="" method="get">
            <input type="text" name="search" id="search" value="" placeholder="Search Here..">
            <div class="column-wrap">
                <div class="column">
                    <label for="year">Year</label>
                    <input type="number" name="year" id="year">
                </div>
                <div class="column">
                    <label for="rating">IMDB Rating</label>
                    <select name="rating" id="rating">
                        <option value="">Any Rating</option>
                        <option value="9">At least 9</option>
                        <option value="8">At least 8</option>
                        <option value="7">At least 7</option>
                        <option value="6">At least 6</option>
                        <option value="5">At least 5</option>
                        <option value="4">At least 4</option>
                        <option value="3">At least 3</option>
                        <option value="2">At least 2</option>
                        <option value="1">At least 1</option>
                    </select>
                </div>
            </div>
            <div class="column-wrap">
                <div class="column">
                    <label for="language">Language</label>
                    <select name="language" id="language">
                        <option value="">Any Language</option>
                        <option value="english">English</option>
                        <option value="korean">Korean</option>
                        <option value="hindi">Hindi</option>
                        <option value="serbian">Serbian</option>
                        <option value="malayalam">Malayalam</option>
                    </select>
                </div>
                <div class="column">
                    <label for="genre">Genre</label>
                    <select name="genre" id="genre">
                        <option value="">Any Genre</option>
                        <option value="action">Action</option>
                        <option value="comedy">Comedy</option>
                        <option value="drama">Drama</option>
                        <option value="horror">Horror</option>
                        <option value="romance">Romance</option>
                    </select>
                </div>
            </div>
            <input type="submit" id="submit" name="submit" value="Search">
        </form>
        <ul id="ajax_fitler_search_results"></ul>
    </div>
	
	<?php
    return ob_get_clean();
}

add_shortcode ('my_ajax_filter_search', 'my_ajax_filter_search_shortcode');

Here, we added 5 fields. The search as an input field, Year as a number field, Rating, Language and Genre as a select dropdown. The options in the drop-down fields are set based on the option we had in the custom fields. The year field will have 4 digit number so it is ok with as a number field. Rating is set from 1 to 9 as IMDB ratings are like that. Language and Genre are ordinary select fields with matching options from the backend custom field and categories.

Adding Necessary Script

We are going to create a new script.js file in our theme folder for adding our javascript codes. Add the following code before the shortcode function we added earlier on the functions.php file.

<?php
// Scripts for Ajax Filter Search

function my_ajax_filter_search_scripts() {
    wp_enqueue_script( 'my_ajax_filter_search', get_stylesheet_directory_uri(). '/script.js', array(), '1.0', true );
    wp_localize_script( 'my_ajax_filter_search', 'ajax_url', admin_url('admin-ajax.php') );
}

We also used wp_localize_script() for easy use of admin-ajax.php file.

Next thing to do is calling the my_ajax_filter_search_scripts() function inside the my_ajax_filter_search_shortcode() function.

<?php
// Shortcode: [my_ajax_filter_search]
function my_ajax_filter_search_shortcode() {

	my_ajax_filter_search_scripts(); // Added here
	
    ob_start(); ?>
 
    <div id="my-ajax-filter-search">
        <form action="" method="get">

		....
		....

This way the scripts needed for the ajax filter/search will load whenever we use the shortcode.

Preaparing Data for Sending the Request

Add the following code to the script.js file

$ = jQuery;

var mafs = $("#my-ajax-filter-search"); 
var mafsForm = mafs.find("form"); 

mafsForm.submit(function(e){
    e.preventDefault(); 

    console.log("form submitted");

// we will add codes above this line later
});

Here we added two variables for ease of use and then a function that fires when the form submit button is pressed. The code e.preventDefault(); part prevents page reload. You will also see output “form submitted” on browser console (Press F12 on Chrome to go to developer tools and then Esc to go to console window).

Now that we see our code is working fine, we can go ahead and prepare the data. Replace console.log(“form submitted”); with the following code (before “});” at the bottom).

    if(mafsForm.find("#search").val().length !== 0) {
        var search = mafsForm.find("#search").val();
    }
    if(mafsForm.find("#year").val().length !== 0) {
        var year = mafsForm.find("#year").val();
    }
    if(mafsForm.find("#rating").val().length !== 0) {
        var rating = mafsForm.find("#rating").val();
    }
    if(mafsForm.find("#language").val().length !== 0) {
        var language = mafsForm.find("#language").val();
    }
    if(mafsForm.find("#genre").val().length !== 0) {
        var genre = mafsForm.find("#genre").val();
    }

    var data = {
        action : "my_ajax_filter_search",
        search : search,
        year : year,
        rating : rating,
        language : language,
        genre : genre
    }

Here, we are checking on submission, if the fields have value. If they do, we set their respective value on a variable and add all of them to the variable data. The part action : “my_ajax_filter_search” is needed for the callback in the next step as we will be sending data via GET method.

Prepare Callback Function

We have the data ready for the request. Now, we need to configure the callback. Going back to the functions.php file, we will add a new function for the callback.

<?php

// Ajax Callback

add_action('wp_ajax_my_ajax_filter_search', 'my_ajax_filter_search_callback');
add_action('wp_ajax_nopriv_my_ajax_filter_search', 'my_ajax_filter_search_callback');

function my_ajax_filter_search_callback() {

    header("Content-Type: application/json"); 

    $meta_query = array('relation' => 'AND');

    if(isset($_GET['year'])) {
        $year = sanitize_text_field( $_GET['year'] );
        $meta_query[] = array(
            'key' => 'year',
            'value' => $year,
            'compare' => '='
        );
    }

    if(isset($_GET['rating'])) {
        $rating = sanitize_text_field( $_GET['rating'] );
        $meta_query[] = array(
            'key' => 'rating',
            'value' => $rating,
            'compare' => '>='
        );
    }

    if(isset($_GET['language'])) {
        $language = sanitize_text_field( $_GET['language'] );
        $meta_query[] = array(
            'key' => 'language',
            'value' => $language,
            'compare' => '='
        );
    }

    $tax_query = array();

    if(isset($_GET['genre'])) {
        $genre = sanitize_text_field( $_GET['genre'] );
        $tax_query[] = array(
            'taxonomy' => 'category',
            'field' => 'slug',
            'terms' => $genre
        );
    }

    $args = array(
        'post_type' => 'movie',
        'posts_per_page' => -1,
        'meta_query' => $meta_query,
        'tax_query' => $tax_query
    );

    if(isset($_GET['search'])) {
        $search = sanitize_text_field( $_GET['search'] );
        $search_query = new WP_Query( array(
            'post_type' => 'movie',
            'posts_per_page' => -1,
            'meta_query' => $meta_query,
            'tax_query' => $tax_query,
            's' => $search
        ) );
    } else {
        $search_query = new WP_Query( $args );
    }

    if ( $search_query->have_posts() ) {

        $result = array();

        while ( $search_query->have_posts() ) {
    		$search_query->the_post();

            $cats = strip_tags( get_the_category_list(", ") );
            $result[] = array(
                "id" => get_the_ID(),
                "title" => get_the_title(),
                "content" => get_the_content(),
                "permalink" => get_permalink(),
                "year" => get_field('year'),
                "rating" => get_field('rating'),
                "director" => get_field('director'),
                "language" => get_field('language'),
                "genre" => $cats,
                "poster" => wp_get_attachment_url(get_post_thumbnail_id($post->ID),'full')
            );
    	}
    	wp_reset_query();

        echo json_encode($result);

    } else {
    	// no posts found
    }
    wp_die();
}

The above code block has few important things that I would like to explain.

  • At line 05 and 06, you see we used add_action for the callback function my_ajax_filter_search_callback(). If you have been using a diffrent function name, you can change wp_ajax_my_ajax_filter_search to wp_ajax_YOUR_FUNCTION_NAME. Same thing for wp_ajax_nopriv_my_ajax_filter_search
  • At line 10 we added header(“Content-Type: application/json”); to declare the content type as json because, we are going to use JSON to send result back to the browser.
  • At line 12 we started adding meta_query rules based on our fields. They are checked if they are set from the form submission then, they are sanitized for injection and compared different conditions.
  • At line 41 we added tax_query rules as we used WordPress categories as the Genre of the movies.
  • Finally, at line 52, we combined the arguments for the query but we also need to query for the search keywords passed from the search input. For this, we used ‘s’ => $search. Here the value of $search is checked for relevant results.
  • After the query, at line 95 we echo the result encoded as JSON which we will use in the next step in jQuery Ajax function.

jQuery Ajax Function

Now we are going to use jQuery Ajax function to complete our last step. Add the following code after the previous code (before the bottom “});”) on script.js file.

$.ajax({
        url : ajax_url,
        data : data,
        success : function(response) {
            mafs.find("ul").empty();
            if(response) {
                for(var i = 0 ;  i < response.length ; i++) {
                     var html  = "<li id='movie-" + response[i].id + "'>";
                         html += "  <a href='" + response[i].permalink + "' title='" + response[i].title + "'>";
                         html += "      <img src='" + response[i].poster + "' alt='" + response[i].title + "' />";
                         html += "      <div class='movie-info'>";
                         html += "          <h4>" + response[i].title + "</h4>";
                         html += "          <p>Year: " + response[i].year + "</p>";
                         html += "          <p>Rating: " + response[i].rating + "</p>";
                         html += "          <p>Language: " + response[i].language + "</p>";
                         html += "          <p>Director: " + response[i].director + "</p>";
                         html += "          <p>Genre: " + response[i].genre + "</p>";
                         html += "      </div>";
                         html += "  </a>";
                         html += "</li>";
                     mafs.find("ul").append(html);
                }
            } else {
                var html  = "<li class='no-result'>No matching movies found. Try a different filter or search keyword</li>";
                mafs.find("ul").append(html);
            }
        } 
    });

Here you can see we used ajax_url that we defined on the functions.php file. We then used the data variable for data param and for the success we have set a response.

As we are going to search multiple times from the same page, we will need to empty the ul that will contain all the results before appending the results. So we used mafs.find(“ul”).empty();

At last, we did a for loop and prepared the layout of the result to the html variable.

mafs.find(“ul”).append(html); appends the result in the ul.

Let us add some quick CSS so that it all looks good. You can add it to the sytle.css file or custom CSS in customizer.

.column-wrap {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    margin: 20px 0
}

.column-wrap .column {
    width: 49%;
}

.column-wrap .column:last-child {
    float: right;
}

#my-ajax-filter-search label {
    display: block;
    font-weight: bold;
    font-style: italic;
}

#my-ajax-filter-search select {
    width: 100%;
    background-color: #f7f7f7;
    padding: 0.625em 0.4375em;
    border: 1px solid #d1d1d1;
}

#ajax_fitler_search_results {
    list-style: none;
    display: flex;
    justify-content: start;
    flex-wrap: wrap;
    margin-top: 30px;
}

#ajax_fitler_search_results li {
    width: 23.5%;
    float: left;
    margin-right: 2%;
    overflow: hidden;
    position: relative;
    margin-bottom: 20px;
}

#ajax_fitler_search_results li.no-result {
    width: 100%;
    text-align: center;
}

#ajax_fitler_search_results li:nth-child(4n+4) {
    margin-right: 0;
}

.movie-info h4 {
    margin-bottom: 10px;
    color: #fff;
}

.movie-info p {
    margin-bottom: 0;
    color: #fff;
    font-size: 13px;
}

.movie-info {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    padding: 15px;
    background: rgba(0, 0, 0, 0.85);
    opacity: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    transition: all .3s;
}

.movie-info * {
    width: 100%;
    display: block;
}

#ajax_fitler_search_results li:hover .movie-info {
    opacity: 1;
}

The Output!

 

Hopefully, this article will help you try Ajax on your next WordPress project. Feel free to share your feedback below. Happy Coding!

Sharing is caring!