WordPress Front-end AJAX Pagination with Search and Sort

I posted a tutorial a year ago about creating an AJAX Pagination in your WordPress front-end, in this tutorial I would like to show how you can integrate a Search Filter and Sorting functionality. This tutorial also covers how you can go back to previous page state (pagination number and applied sorting filter) after visiting a post then clicking the back button in your browser.

wordpress-front-end-ajax-pagination-with-search-and-sort-screenshot
Demo

Step 1: Create a custom page in WordPress

  • Go to your Dashboard > Pages > Add New
  • Name the page anything you want, ex: My Posts
  • In your Dashboard > Settings > Permalinks, make sure Common Settings is set to Post Name
  • In your newly created page, copy the page slag: if you used “My Posts” as your page name, the slag would be my-posts
  • In your WordPress theme create a file called page-my-posts.php  Notice how we attached the slag to the “page-“, this will allow us to add custom scripts that will only apply to this specific page.
  • Go to your browser and navigate to your new page. ex. http://example.com/my-posts It should show you a blank white page, if not then you did something incorrectly.

Step 2: Working on our page

  • Open the new page and paste the following code template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php get_header(); ?>
    <div class  ="col-md-12 content">
        <div class = "content">
            <form class = "post-list">
                <input type = "hidden" value = "" />
            </form>
            <?php if ( have_posts() ) while ( have_posts() ) : the_post(); ?>
                <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                    <h1 class="entry-title"><?php the_title(); ?></h1>                                                                                 
                    <hr />
                    <article class="entry-content clear">
                        <?php the_content(); ?>
                    </article>
                </article>
            <?php endwhile; ?> 
           
            <article class="navbar-form navbar-left">
                <div class="form-group">
                    <input type="text" class="form-control post_search_text" placeholder="Enter a keyword">
                </div>
                <input type = "submit" value = "Search" class = "btn btn-success post_search_submit" />
            </article>
           
            <br class = "clear" />
           
            <script type="text/javascript">
            var ajaxurl = '<?php echo admin_url('admin-ajax.php'); ?>';
           
            function cvf_load_all_posts(page, th_name, th_sort){
                $(".cvf_universal_container").html('<p><img src = "<?php bloginfo('template_url'); ?>/images/loading.gif" class = "loader" /></p>');
               
                var post_data = {
                    page: page,
                    search: $('.post_search_text').val(),
                    th_name: th_name,
                    th_sort: th_sort
                };
               
                $('form.post-list input').val(JSON.stringify(post_data));
               
                var data = {
                    action: "demo_load_my_posts",
                    data: JSON.parse($('form.post-list input').val())
                };
               
                $.post(ajaxurl, data, function(response) {
                    if($(".cvf_universal_container").html(response)){
                        $('.table-post-list th').each(function() {
                            // Append the button indicator
                            $(this).find('span.glyphicon').remove();   
                            if($(this).hasClass('active')){
                                if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){
                                    $(this).append(' <span class="glyphicon glyphicon-chevron-down"></span>');
                                } else {
                                    $(this).append(' <span class="glyphicon glyphicon-chevron-up"></span>');
                                }
                            }
                        });
                    }
                });
            }
           
            jQuery(document).ready(function($) {                                                               
               
                // Initialize default item to sort and it's sort order
               
                // Check if our hidden form input is not empty, meaning it's not the first time viewing the page.
                if($('form.post-list input').val()){
                    // Submit hidden form input value to load previous page number
                    data = JSON.parse($('form.post-list input').val());
                    cvf_load_all_posts(data.page, data.th_name, data.th_sort);
                } else {
                    // Load first page
                    cvf_load_all_posts(1, 'post_title', 'ASC');
                }
               
                var th_active = $('.table-post-list th.active');
                var th_name = $(th_active).attr('id');
                var th_sort = $(th_active).hasClass('DESC') ? 'ASC': 'DESC';
                           
                // Search
                $('body').on('click', '.post_search_submit', function(){
                    cvf_load_all_posts(1, th_name, th_sort);
                });
                // Search when Enter Key is triggered
                $(".post_search_text").keyup(function (e) {
                    if (e.keyCode == 13) {
                        cvf_load_all_posts(1, th_name, th_sort);
                    }
                });
               
                // Pagination Clicks                   
                $('.cvf_universal_container .cvf-universal-pagination li.active').live('click',function(){
                    var page = $(this).attr('p');
                    var current_sort = $(th_active).hasClass('DESC') ? 'DESC': 'ASC';
                    cvf_load_all_posts(page, th_name, current_sort);               
                });

                // Sorting Clicks
                $('body').on('click', '.table-post-list th', function(e) {
                    e.preventDefault();                            
                    var th_name = $(this).attr('id');
                                                       
                    if(th_name){
                        // Remove all TH tags with an "active" class
                        if($('.table-post-list th').removeClass('active')) {
                            // Set "active" class to the clicked TH tag
                            $(this).addClass('active');
                        }
                        if(!$(this).hasClass('DESC')){
                            cvf_load_all_posts(1, th_name, 'DESC');
                            $(this).addClass('DESC');
                        } else {
                            cvf_load_all_posts(1, th_name, 'ASC');
                            $(this).removeClass('DESC');
                        }
                    }
                })
            });
            </script>
           
            <table class = "table table-striped table-post-list no-margin">
                <tr>
                    <th width = "25%" class = "active" id = "post_title"><u><a href = "#">Post Name</a></u></th>
                    <th width = "60%">Description</th>
                    <th width = "15%" id = "post_date"><u><a href = "#">Post Date</a></u></th>
                </tr>
            </table>
           
            <div class = "cvf_pag_loading no-padding">
                <div class = "cvf_universal_container">
                    <div class="cvf-universal-content"></div>
                </div>
            </div>
        </div>
    </div>
   
<?php get_footer(); ?>
  • Explanations are on the codes
  • This tutorial make use of bootstrap classes, so I suggest including bootstrap on the header part of your website:
1
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />

Step 3: Add the following PHP Code to your functions.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
add_action( 'wp_ajax_demo_load_my_posts', 'demo_load_my_posts' );
add_action( 'wp_ajax_nopriv_demo_load_my_posts', 'demo_load_my_posts' );
function demo_load_my_posts() {
       
    global $wpdb;
   
    $msg = '';
   
    if( isset( $_POST['data']['page'] ) ){
        // Always sanitize the posted fields to avoid SQL injections
        $page = sanitize_text_field($_POST['data']['page']); // The page we are currently at
        $name = sanitize_text_field($_POST['data']['th_name']); // The name of the column name we want to sort
        $sort = sanitize_text_field($_POST['data']['th_sort']); // The order of our sort (DESC or ASC)
        $cur_page = $page;
        $page -= 1;
        $per_page = 15; // Number of items to display per page
        $previous_btn = true;
        $next_btn = true;
        $first_btn = true;
        $last_btn = true;
        $start = $page * $per_page;
       
        // The table we are querying from  
        $posts = $wpdb->prefix . "posts";
       
        $where_search = '';
       
        // Check if there is a string inputted on the search box
        if( ! empty( $_POST['data']['search']) ){
            // If a string is inputted, include an additional query logic to our main query to filter the results
            $where_search = ' AND (post_title LIKE "%%' . $_POST['data']['search'] . '%%" OR post_content LIKE "%%' . $_POST['data']['search'] . '%%") ';
        }
       
        // Retrieve all the posts
        $all_posts = $wpdb->get_results($wpdb->prepare("
            SELECT * FROM $posts WHERE post_type = 'post' AND post_status = 'publish' $where_search
            ORDER BY $name $sort LIMIT %d, %d"
, $start, $per_page ) );
       
        $count = $wpdb->get_var($wpdb->prepare("
            SELECT COUNT(ID) FROM "
. $posts . " WHERE post_type = 'post' AND post_status = 'publish' $where_search", array() ) );
       
        // Check if our query returns anything.
        if( $all_posts ):
            $msg .= '<table class = "table table-striped table-hover table-file-list">';
           
            // Iterate thru each item
            foreach( $all_posts as $key => $post ):
                $msg .= '
                <tr>
                    <td width = "25%"><a href = "'
. get_permalink( $post->ID ) . '">' . $post->post_title . '</a></td>
                    <td width = "60%">'
. $post->post_excerpt . '</td>
                    <td width = "15%">'
. $post->post_date . '</td>
                </tr>'
;        
            endforeach;
           
            $msg .= '</table>';
       
        // If the query returns nothing, we throw an error message
        else:
            $msg .= '<p class = "bg-danger">No posts matching your search criteria were found.</p>';
           
        endif;

        $msg = "<div class='cvf-universal-content'>" . $msg . "</div><br class = 'clear' />";
       
        $no_of_paginations = ceil($count / $per_page);

        if ($cur_page >= 7) {
            $start_loop = $cur_page - 3;
            if ($no_of_paginations > $cur_page + 3)
                $end_loop = $cur_page + 3;
            else if ($cur_page <= $no_of_paginations && $cur_page > $no_of_paginations - 6) {
                $start_loop = $no_of_paginations - 6;
                $end_loop = $no_of_paginations;
            } else {
                $end_loop = $no_of_paginations;
            }
        } else {
            $start_loop = 1;
            if ($no_of_paginations > 7)
                $end_loop = 7;
            else
                $end_loop = $no_of_paginations;
        }
         
        $pag_container .= "
        <div class='cvf-universal-pagination'>
            <ul>"
;

        if ($first_btn && $cur_page > 1) {
            $pag_container .= "<li p='1' class='active'>First</li>";
        } else if ($first_btn) {
            $pag_container .= "<li p='1' class='inactive'>First</li>";
        }

        if ($previous_btn && $cur_page > 1) {
            $pre = $cur_page - 1;
            $pag_container .= "<li p='$pre' class='active'>Previous</li>";
        } else if ($previous_btn) {
            $pag_container .= "<li class='inactive'>Previous</li>";
        }
        for ($i = $start_loop; $i <= $end_loop; $i++) {

            if ($cur_page == $i)
                $pag_container .= "<li p='$i' class = 'selected' >{$i}</li>";
            else
                $pag_container .= "<li p='$i' class='active'>{$i}</li>";
        }
       
        if ($next_btn && $cur_page < $no_of_paginations) {
            $nex = $cur_page + 1;
            $pag_container .= "<li p='$nex' class='active'>Next</li>";
        } else if ($next_btn) {
            $pag_container .= "<li class='inactive'>Next</li>";
        }

        if ($last_btn && $cur_page < $no_of_paginations) {
            $pag_container .= "<li p='$no_of_paginations' class='active'>Last</li>";
        } else if ($last_btn) {
            $pag_container .= "<li p='$no_of_paginations' class='inactive'>Last</li>";
        }

        $pag_container = $pag_container . "
            </ul>
        </div>"
;
       
        echo
        '<div class = "cvf-pagination-content">' . $msg . '</div>' .
        '<div class = "cvf-pagination-nav">' . $pag_container . '</div>';
       
    }
   
    exit();
   
}

Step 4: Add some CSS styling to our Pagination navigation:

  • Include the following code in your style.css:
1
2
3
4
5
6
.cvf_pag_loading {padding: 20px; }
.cvf-universal-pagination ul {margin: 0; padding: 0;}
.cvf-universal-pagination ul li {display: inline; margin: 3px; padding: 4px 8px; background: #FFF; color: black; }
.cvf-universal-pagination ul li.active:hover {cursor: pointer; background: #1E8CBE; color: white; }
.cvf-universal-pagination ul li.inactive {background: #7E7E7E;}
.cvf-universal-pagination ul li.selected {background: #1E8CBE; color: white;}


Do you need help with a project? or have a new project in mind that you need help with?

Contact Me