How to Create an AJAX powered Sortable Table in WordPress

I recently had to create a sortable and filtered AJAX post list in WordPress. I learned a lot, so I figured I’d post how I did it in hopes of helping someone else!

To get started using AJAX in WordPress I followed this excellent tutorial, “Getting Loopy – Ajax Powered Loops with jQuery and WordPress”, but it uses AJAX to create an infinite scroll. For our example below we are going to be sorting posts by title and rank, and also adding the ability to filter by color. Rank and color are added to the post as custom meta.

Step 1

First we’ll set up our main loop with a table and add pagination. The table header contains a select form element listing all the available colors to choose from.

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
<table class="index">
  <thead>
    <tr>
      <th class="rank selected"><a id="rank-sort" class="DESC" href="?orderby=rank&order=ASC">Score</a></th>
      <th class="title"><a id="title-sort" href="?orderby=title&order=ASC">Title</a></th>
      <th class="color">
        <form class="custom">
          <select id="customDropdown">
          <option>All colors</option>
          <?php 
          $colors = $wpdb->get_col("SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'color'" );
          $colors = array_unique($colors);
          sort($colors);
          foreach ($colors as $val) {
            echo "<option value='" . $val . "'>" . $val . "</option>";
          }
          ?>
          </select>
        </form>
      </th>
    </tr>
  </thead>
  <tbody class="tbody">
  <?php while ( have_posts() ) : the_post(); ?>
    <tr>
      <td class="rank"><?php echo get_metadata( 'post', get_the_ID(), 'rank', true );?></td>
      <td class="title"><a class="title" href="<?php the_permalink(); ?>"><?php the_title(); ?></a></td>
      <td class="state"><?php echo get_metadata( 'post', get_the_ID(), 'color', true ); ?></td>
    </tr>
  <?php endwhile; ?>
    <tr>
      <td colspan="5">
        <div class="pagination">
        <?php 
        echo $wp_query->paged;
        echo paginate_links(array(  
          'base'     => '%_%',  
          'format'   => '/page/%#%',
          'show_all' => true,
          'current'  => max( 1, get_query_var('paged') ),  
          'total'    => $wp_query->max_num_pages
        ));  ?>
        </div>
      </td>
    </tr>
  </tbody>
</table>

Step 2

In our functions.php file we’ll modify the homepage query to sort by rank by default, showing 10 posts at a time.

1
2
3
4
5
6
7
8
9
10
add_action( 'pre_get_posts', 'custom_modify_home' );
function custom_modify_home( $query ) {
  if ($query->is_front_page() && $query->is_main_query()) {
    $query->set('post_status', 'publish');
    $query->set('meta_key','rank');
    $query->set('orderby','meta_value_num');
    $query->set('order','DESC');
    $query->set('posts_per_page', 10);
  }
}

Step 3

Then we’ll create a javascript file containing the AJAX call to handle the sorting and filtering. I called the file ajaxLoop.js

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
jQuery(function($){
  var sortRank = '0';
  var sortTitle = '0';
  var selectedColor = '0';
  var page = 1;
  var loading = true;
  var $content = $("body .tbody");
  var load_posts = function(){
    $.ajax({
      type : "GET",
      data : {action: "custom_loop_handler", "rank": sortRank, "title": sortTitle, "color": selectedColor, "pageNumber": page},
      dataType : "html",
      url : myAjax.ajaxurl,
      success : function(data){
        $("body .tbody").html(data);
        $('.ajax-paginate a').click(function() {
          if ($(this).hasClass('prev')) {
                        page--;
          } else if ($(this).hasClass('next')) {
            page++;
          } else {
            page = $(this).text();
          }
          load_posts();
          return false;
        });
      },
      error : function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR + " :: " + textStatus + " :: " + errorThrown);
      }
    });
  }
});

This actually doesn’t do anything on it’s own, we need to add event handlers to initiate load_posts() and pass the correct preferences along.

Step 4

We add the javascript below to ajaxLoop.js to pass the user’s choices to our AJAX request and tell it to replace the content in <tbody>. This code also adds ‘selected’ classes to the table headers so we can show the user which column they’re currently sorting/filtering by.

Color Dropdown:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$('#customDropdown').change(function() {
  // Get the selected choice
  selectedColor = $(this).attr('value');
 
  // If one of our other filters had a setting, get and pass along w/ color choice change
  if (!$('#title-sort').parents('th').hasClass('selected')) {
    sortTitle = '0';
  } else {
    sortTitle = $('#title-sort').attr("class");
  }
 
  if (!$('#rank-sort').parents('th').hasClass('selected')) {
    sortRank = '0';
  } else {
    sortRank = $('#rank-sort').attr("class");
  }
 
  // Reset page number to 1
  page = 1;
  load_posts();
  // This is the first choice, hide standard pagination (replacing w/ AJAX powered pagination)
  $('.pagination').hide();
    return false;
});

Title:

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
$('#title-sort').click(function() {
  // Reset other sort
  sortRank = '0';
  // This selector is now active, highlight table cell
  $(this).parents('th').addClass('selected').siblings('th').removeClass('selected');
 
  // On first click this won't have an asc or desc class to get, so we need to add it
  sortTitle = $(this).attr("class");
  if (!sortTitle) {
    sortTitle = 'ASC';
    $(this).addClass('ASC');
  } else {
    // Change sort class
    $(this).toggleClass('ASC DESC');
  }
 
  // Reset page number to 1
  page = 1;
  // Store sort class for ajax
  sortTitle = $(this).attr("class");
 
  load_posts();
  // Just in case this is the first choice, hide standard pagination (replacing w/ ajax)
  $('.pagination').hide();
  return false;
});

Rank:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$('#rank-sort').click(function() {
  // Reset other sort
  sortTitle = '0';
 
  // This selector is no longer inactive, highlight table cell
  $(this).parents('th').addClass('selected').siblings('th').removeClass('selected');
 
  // Reset page number to 1
  page = 1;
 
  // Change sort class
  $(this).toggleClass('ASC DESC');
 
  // Store sort class for ajax
  sortRank = $(this).attr("class");
 
  load_posts();
  // Just in case this is the first choice, hide standard pagination (replacing w/ ajax)
  $('.pagination').hide();
 
  return false;
});

Pagination:

Our example also includes AJAX powered pagination, which you could optionally remove and just display all results at once. The following handles the pagination link clicks, replacing them with AJAX pagination.

1
2
3
4
5
6
7
8
9
10
11
12
13
// if original pagination link
$('.pagination a').click(function() {
  if ($(this).hasClass('prev')) {
  	page--;
  } if ($(this).hasClass('next')) {
    page++;
  } else {
    page = $(this).text();
  }
  $('.pagination').hide();
  load_posts();
  return false;
});

Step 5

We can add this JS file to WordPress in our functions.php. ‘myAjax’ is called in step 3, line 13 so these need to match!

1
2
3
4
5
6
7
8
add_action('wp_enqueue_scripts', 'register_ajaxLoop_script');
function register_ajaxLoop_script() {
   wp_register_script( 'ajaxLoop', get_stylesheet_directory_uri() . '/js/ajaxLoop.js', array('jquery') );
   wp_localize_script( 'ajaxLoop', 'myAjax', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ) ) );        
 
   wp_enqueue_script( 'jquery' );
   wp_enqueue_script( 'ajaxLoop' );   
}

Now that we are passing the choices via AJAX we need to create a function to change the main query and send back new content. This is added to functions.php, you’ll notice the name, custom_loop_handler, is the same as step 3, line 11.

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
<?php
add_action( 'wp_ajax_custom_loop_handler', 'custom_loop_handler' );
add_action( 'wp_ajax_nopriv_custom_loop_handler', 'custom_loop_handler' );
 
function custom_loop_handler() {
  $pageVAR = (isset($_GET['pageNumber'])) ? $_GET['pageNumber'] : 0;
 
  /* IF COLOR IS SET */ 
  if (($_GET['color'] != '0') && ($_GET['color'] != 'All colors')) {
    $colorValue = $_GET['color'];
    $colorQuery = array( array(
      'key'   => 'color',
      'value' => $colorValue
    ));
  } else {
    $colorQuery = array( array(
      'key'     => 'color',
      'compare' => 'EXISTS'
    ));
  }
 
  /* IF TITLE-SORT IS SET */ 
  if ($_GET['title'] != '0') {
    $orderby = 'title';
    $order = $_GET['title'];
  } else {
    $orderby = 'meta_value_num';
    $order = 'DESC';
  }
 
  /* IF RANK-SORT IS SET */
  if ($_GET['rank'] != '0') {
    $orderby = 'meta_value_num';
    $order = $_GET['rank'];
  } else if ($_GET['title'] == '0') {
    $orderby = 'meta_value_num';
    $order = 'DESC';
  }
 
  $my_query = new WP_Query();
  $my_query->query(array(
    'posts_per_page' => 10,
    'orderby'        => $orderby,
    'order'          => $order,	
    'meta_key'       => 'rank',
    'meta_query'     => $colorQuery,
    'paged'          => $pageVAR
  )); 
 
  $totalpages = $my_query->max_num_pages;
 
  /* Prev button is current page +1 (unless the last post displayed is more than the total posts, then don't display) */
  if ($pageVAR >= $totalpages) {
    $prevpage = 0;
  } else {
    $prevpage = $pageVAR + 1;
  }
  /* Next button is current page -1 */
  $nextpage = $pageVAR - 1;
 
  if ($my_query->have_posts()) : while ($my_query->have_posts()) : $my_query->the_post(); ?>
 
  <tr>
    <td class="rank"><?php echo get_metadata( 'post', get_the_ID(), 'rank', true ); ?></td>
    <td class="title"><a class="title" href="<?php the_permalink(); ?>"><?php the_title(); ?></a></td>
    <td class="color"><?php echo get_metadata( 'post', get_the_ID(), 'color', true ); ?></td>
  </tr>
  <?php endwhile; ?>
  <tr>
    <td colspan="5">
    <?php if ($totalpages > 1) { 
      echo '<div class="ajax-paginate">';
      echo paginate_links(array(  
        'base'     => '%_%',  
        'format'   => '/page/%#%',
        'show_all' => true,
        'current'  => $pageVAR,  
        'total'    => $totalpages,  
      )); 
      echo '</div>'; 
    }   ?>
    </td>
  </tr>
  <?php endif;
        die();
  }
?>

If you have any questions or ideas on how to improve, please let me know in the comments!

Download Complete Example Demo
(Updated 10/29/31)

11 Responses to “How to Create an AJAX powered Sortable Table in WordPress”

  1. agep roem

    Hey Alicia… Thank you for this tut.. ajaxing wordpress tut is very rare on the internet..
    but I have a question
    why I always get 0..
    and do you have a demo for this tut..

    Reply
      • hipoteca ing prestamo personal

        Well, despite the dreadful weather, you have managed to bring some Mexican colour and glamour to the British summer, Vix! A wonderful tribute to Frida, love that gingham skirt, what a bargain. Such a great parcel from Tamera, and there are more lovely gifts too! You get back what you give in life, which is why you get so many delightful parcels from friends. The pendant is amazing, and I love the look of the fabric.Oh look at darling Stephen Squirrel, cats ALWAYS sit on your stuff, it's their awkward instinct!SOOO excited!!! xxxxxxxx

        Reply
  2. Arul

    This is very useful indeed. I am trying ajax sort comments with meta values. I guess just replace new_WP_query with comment query and I would have a decent platform to build on,

    May be you could add to it(next tut may be)

    Thanks.

    Reply
  3. Alexander

    Hi Alicia, thanks for the great tutorial.
    Only I have 1 problem. When i click on next button (or a number) from the pagination. It changes the content perfectly, but the pagination controls don’t change. So I never see the previous button and I can click unlimited on next… Do you have any idea what the problem could be?

    Reply
    • Cindy

      Meeeen! Jag var ocksÃ¥ där pÃ¥ lördagen, men smög mest runt som en lÃ¥ng vandrande pinne. :) SÃ¥g inte till nÃ¥got vilt härjande, sÃ¥ vi mÃ¥ste ha gÃ¥tt om varandra tis¤Ãmsdsigt! Fyndade du nÃ¥got smaskigt eller knöt nÃ¥n kontakt kanske?

      Reply
  4. RCHdriego

    Официальный магазин Чемпион – динамично растущая коммерческая предприятие, функционирующая на Нашем рынке более 6долгих лет.

    Ищите садовую технику для дачного дома, но не ведаетее,какая качественнее и где её взять? Мы вам делаем отличное предложение побывать наш магазин ФМ Чемпион. У нас вас ждёт широкий ассортимент ЗАРЯДНЫЕ УСТРОЙСТВА. Если уж вы приняли решение купить Дизельные электростанции наш магазин покажем вам все модели товара.

    Магазин ФМ Чемпион это то место где можно купить все для дома начиная с Бензиновые мотопомпы и заканчивая Виброплитами.

    Приобрести садовое технику и дополнительно инструменты в указанном интернет-сети магазине на интернет-сайте сегодня можно в рабочие дни и в выходные и праздничные дни Транспортировка любого агрегата при покупки от 9 тысяч рублей происходит по городу бесплатно. Мы готовы предоставить гарантийное обеспечение на все товары, которые конкретно сейчас есть в нашем магазине. Развоз товаров по назначению происходит в течении 1-7 суток с момента покупки. Сроки зависят от города, представленную информацию уточнит продавец.

    Техника Чемпион – снегоуборщик champion st

    Reply

Leave a Comment

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>