Roots Discourse

Ajax Posts by category

On my blog page, I have created a dropdown of all the categories with their url as the value. I’m still learning how to use ajax but I am trying to load posts based on the category selected. After reading this discussion I have gotten my admin ajax to load properly, but when you select a dropdown item it still pulls all posts instead of just the category. I can tell because my “console.log(got);” shows me it grabbed a page with the body class of “blog app-data index-data home-data”. I’m not sure if I have added my helper incorrectly or I am setting up my ajax url incorrectly?

home.php

<h2 class="title-template"><?php echo (is_home() ? __('Blog') : get_the_archive_title()); ?></h2>
 <?php
        $cat_terms = get_terms(array('taxonomy' => 'category'));
      ?>
         <form action="" method="post" id="blog-filter">
        	Filter By <select name="blog_sort" id="blog-cat-menu">
        	    <option value="<?php echo get_permalink(get_option('page_for_posts')); ?>" selected><?php _e('All Categories'); ?></option>
              @foreach($cat_terms as $t) 
                <option value="<?php echo get_term_link($t); ?>"><?php echo $t->name; ?></option>
              @endforeach
        	</select>
        </form>
      <div class="blog-wrap">
    		<ul class="my-blogs">
    			<?php while ( have_posts() ) : the_post(); ?>	
          	       @include('partials.content')
    			<?php endwhile; ?>
    		</ul>
      </div>

setup.php

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_style('sage/main.css', asset_path('styles/main.css'), false, null);
    wp_enqueue_script('sage/main.js', asset_path('scripts/main.js'), ['jquery'], null, true);
    $ajax_params = array(
      'ajax_url' => admin_url( 'admin-ajax.php' ),
      'ajax_nonce' => wp_create_nonce( 'my_nonce' ), 
    );
    wp_localize_script( 'sage/main.js', 'ajax_object', $ajax_params);
}, 100);

common.js

$(document).on('change', '#blog-cat-menu', function() {
        var theForm = $(this);
        $.ajax({
            url:      ajax_object.ajax_url, 
            data:     theForm.serialize() + '&action=filter_posts',
            type:     'POST',
            dataType: 'json',
            beforeSend: function() {
                $('body').addClass('ajax-sort');
            },
            success: function(data) {
                $.get(data.url, function(got) {
                console.log(got); 
                    $('.blog-wrap').replaceWith($(got).find('.blog-wrap'));
                    $('.title-template').replaceWith($(got).find('.title-template'));
                })
                .done(function() {
                    $('body').removeClass('ajax-sort');
                });
            },
        });
        return false;
    });

    console.log(ajax_object);

helpers.php

function ajax_filter_categories() {
  echo json_encode(array('url' => $_POST['blog_sort']));
  die();
}

add_action('wp_ajax_filter_posts', 'ajax_filter_categories');
add_action('wp_ajax_nopriv_filter_posts', 'ajax_filter_categories');

data: theForm.serialize()

Are you serializing the entire form object here? I feel like you need to do something like theForm.value.serialize() so that you’re only using the value from the form submisson. IIRC you can also use the “Network” tab (or the equivalent) in your browsers inspector to inspect the actual AJAX queries, responses, etc.

1 Like

I could be wrong but I think this, “var theForm = $(this);” should cover my form value. I do have this working in another theme that is not sage and am trying to adapt it. I tried your value suggestion but it threw a serialize error. Thanks for the tip on the network tab, I have much to learn.

Hey @Scott_V

It looks like you’re making two AJAX requests here–can you help clarify why?

The first is triggered by the change event and gets a URL value back from your ajax_filter_categories action.

The second call is triggered when the first call successfully completes and loads the URL returned in the first step.

The value returned from the first call is the same value that’s sent to the server in that call, which means this step isn’t needed unless it’s a placeholder for something different later.

If it’s not a placeholder, here’s a simplified version that removes the unnecessary first call and loads the posts from the selected category.

First, you can delete your code you shared from helpers.php. If all you’re doing is calling the category URL to load that page’s HTML and then using JavaScript to extract the posts and inject them into the page, you don’t need any extra code on the server side.

Second, replace your code in common.js with this (or you could put it in a home route so that the listener is only added on that specific template):

$("#blog-cat-menu").change(function() {
  $.ajax({
    url: $(this).val(),
    beforeSend: function() {
      $("body").addClass("ajax-sort");
    },
    success: function(got) {
      $(".blog-wrap").replaceWith($(got).find(".blog-wrap"));
      $(".title-template").replaceWith($(got).find(".title-template"));
      $("body").removeClass("ajax-sort");
    },
  });
});

PS - while this should get the method you’re already using working, there are a couple of other methods that might be better in the long run:

  1. Use an action like you were before to generate just the HTML you need and return it, instead of loading a full page in your AJAX request and then extracting the HTML you need.
  2. Use the WordPress REST API to get the posts from the category and then generate and inject the HTML needed in your JS.

The first option is probably easier to set up.

Loading your entire page in the AJAX request when you just need a slice of it (the section with the posts) adds additional overhead throughout the process. Depending on your specific circumstances, your users may never notice the difference, but you should be aware that there are other, cleaner approaches.

1 Like

I tried your suggestions but when I selected something from the dropdown all the content disappeared and was not replaced. I’m a little confused because I thought I needed to return something physical which is why I use a dataType and helpers. Your version of common.js has the exact same response as my version in the inspector network tab. Definitely cleaner than mine.

This is likely what you’re running into:

You need to make sure that whatever template is loaded when you visit the category URLs used in your select includes the same markup as your home.blade.php (the blog-wrap and title-template elements). In my test case, this was index.blade.php.

This is because your home template isn’t by default the one that’s used when you visit mysite.com/category/my-category/. So, if the template that IS used doesn’t have that markup, your JS won’t be able to find the HTML it’s supposed to use in the response that comes back.

You are returning something just as ‘physical’ with this method as you originally were. Here’s what you were doing before:

  1. Change happens: AJAX call sends mysite.com/category/my-category/ to the server
  2. The server sends mysite.com/category/my-category/ right on back
  3. Your second AJAX call loads the HTML of mysite.com/category/my-category/ (just like if you visited it in your browser) and then uses the pieces it needs to update the page

In the new method, we’re just skipping the first two steps, since it’s completely useless to send a URL to the server just so you can get the exact same URL back.

Finally, dataType is only needed when jQuery can’t autodetect the content type.

If making sure that the template generating HTML being returned from the AJAX call includes the same structure as your home template doesn’t fix it, please follow up with the code from all of the relevant files and an example of one of the URLs in your select, and I’ll take another look.

1 Like

You are correct and that makes total sense. I had something similar to my home.blade on my category.blade but hadn’t updated it since I made some tweaks. Thank you so much for taking the time to explain this. Above and beyond what I expected and much appreciated. I was clearly over thinking with my original method.

1 Like

This is awesome and works great with a drop down. Is it possible to do something similar with an unordered list of links?