I have been delving into AJAX within WordPress and have followed many tutorials on how to do so, but every single request returns a 400 error.
In my functions.php I have added the following:
Step 1 - Register and add scripts to WordPress
/**
* Initialize the JavaScript we need for loading more
*
* @return void
*/
function ajax_load_more_init()
{
// Register and enqueue the script we need for load more
wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
wp_enqueue_script('ajax-load-more-script');
// Localize the script so we can access tthe variables in PHP
wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
'ajax_url' => admin_url('admin-ajax.php'),
));
}
Step 2 - Enqueue the actual script
/**
* Add AJAX loader to scripts
*/
add_action('wp_enqueue_scripts', 'ajax_load_more_init');
Step 3 - Allow AJAX to be used on the frontend
// AJAX Hook for users
add_action('wp_ajax_ajax_load_more', 'ajax_load_more');
add_action('wp_ajax_nopriv_ajax_load_more', 'ajax_load_more');
Step 4 - A dead simple function for testing
/**
* The backend PHP to actually load more posts
*
* @return void
*/
function ajax_load_more()
{
echo "TESTING";
wp_die();
}
In my actual AJAX script I have the following:
jQuery(document).ready(function ($) {
// Initialize Isotope as $grid
const $grid = $('#grid').isotope({
itemSelector: '.grid-item',
percentagePosition: true,
animationEngine: 'best-available', //CSS3 if browser supports it, jQuery otherwise
animationOptions: {
duration: 800,
},
masonry: {
columnWidth: '.grid-item',
gutter: 30,
},
})
var has_run = false;
var init_offset = 0;
// Hook into click event
$('button.load-more-posts').click(function (e) {
e.preventDefault();
var button = $(this);
var nonce = $(this).data("nonce");
console.log('Nonce is: ' + nonce);
// Disable the button
button.prop("disabled", true);
// Check the offset
if (has_run == false) {
button.data('offset', $(this).data("offset"));
init_offset = $(this).data("offset");
}
console.log('Initial offset is: ' + init_offset);
console.log('Initial offset is: ' + button.data('offset'));
// Perform AJAX request
$.ajax({
type: 'POST',
dataType: 'json',
url: ajax_load_more_object.ajax_url,
contentType: 'application/json; charset=utf-8',
data: {
action: 'ajax_load_more',
security: nonce,
init_offset: init_offset,
offset: button.data('offset'),
},
beforeSend: function (xhr) {
console.log('Loading more posts...')
button.text('Loading');
},
success: function (response) {
console.log(response);
// Undo Button Disable
button.prop("disabled", false);
// Set Offset
button.data("offset", offset + 10);
// Script has run
has_run = true;
return false;
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
}
});
});
});
As you can see in the $ajax
call, the url is to admin-ajax
and the action is the trailing part of wp_ajax_nopriv_ajax_load_more
.
The nonce comes from the button on the front end like so:
<button class="load-more-posts" data-nonce="<?php echo wp_create_nonce('load_more_ajax'); ?>" data-offset="10">Load More Posts</button>
My working code
So, thanks to everyone's shared input I was able to come up with a solution that I'll detail below:
In functions.php (should really be in a plugin)
/**
* Additional thumbnail sizes
*/
add_theme_support('post-thumbnails');
add_image_size('post-thumbnail-square', 300, 300, true);
/**
* Initialize the JavaScript we need for loading more
*
* @return void
*/
function ajax_load_more_init()
{
// Register and enqueue the script we need for load more
wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
wp_enqueue_script('ajax-load-more-script');
// Localize the script so we can access tthe variables in PHP
wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
'ajax_url' => admin_url('admin-ajax.php'),
));
}
/**
* Add AJAX loader to scripts
*/
add_action('wp_enqueue_scripts', 'ajax_load_more_init');
/**
* Allow AJAX to be used on the front end
*/
add_action('wp_ajax_ajax_load_more_posts', 'ajax_load_more_posts_callback');
add_action('wp_ajax_nopriv_ajax_load_more_posts', 'ajax_load_more_posts_callback');
/**
* The backend PHP to actually load more posts
*
* @return void
*/
function ajax_load_more_posts_callback()
{
// First check the nonce, if it fails the function will break
check_ajax_referer('ajax_load_more_posts', 'security');
// Get the data we have from the load more button
$offset = $_POST['offset'];
$init_offset = $_POST['init_offset'];
// Get posts with given offset
if ($offset != null && absint($offset) && $init_offset != null && absint($init_offset)) {
// Finally, we'll set the query arguments and instantiate WP_Query
$args = array(
'post_type' => 'post',
'posts_per_page' => $init_offset,
'offset' => $offset
);
$post_list = array();
$query = new WP_Query($args);
if ($query->have_posts()) :
while ($query->have_posts()) : $query->the_post();
//$categories = get_the_categories();
$post_list[] = array(
'category' => get_the_category(),
'title' => get_the_title(),
'introduction' => get_field('introduction'),
'date' => get_the_date(),
'permalink' => get_permalink(),
'thumbnail' => get_the_post_thumbnail(),
);
endwhile;
endif;
echo json_encode($post_list);
wp_die();
}
}
In my ajax-loader script
jQuery(document).ready(function ($) {
var has_run = false;
var init_offset = 0;
// Hook into click event
$('button.load-more-posts').click(function (e) {
var $grid = $('#grid').isotope({
itemSelector: '.grid-item',
});
e.preventDefault();
var button = $(this);
var nonce = $(this).data("nonce");
// Disable the button
button.prop("disabled", true);
// Check the offset
if (has_run == false) {
button.data('offset', $(this).data("offset"));
init_offset = $(this).data("offset");
}
// Perform AJAX request
$.ajax({
type: 'POST',
dataType: 'json',
url: ajax_load_more_object.ajax_url,
data: {
action: 'ajax_load_more_posts',
security: nonce,
init_offset: init_offset,
offset: button.data('offset'),
},
beforeSend: function (xhr) {
console.log('Loading more posts...')
button.text('Loading');
},
success: function (response) {
console.log(response);
button.text('Load more');
// An array to store new items added via AJAX
var new_items = [];
// Run through JSON
$.each(response, function (key, value) {
var $new_item = $(`<div class="grid-item article-post-card ${value.category[0]['slug']}">
<a class="article-post-card__anchor" href=" ${value.permalink}" alt="${value.title}">
<div class="article-post-card__featured-image-container">
<div class="article-post-card__overlay"></div>
<div class="article-post-card__featured-image">
${value.thumbnail}
</div>
<div class="article-post-card__category-label">
${value.category[0]['name']}
</div>
</div>
<div class="article-post-card__content-wrapper">
<div class="article-post-card__publish-date">
<time class="updated" datetime="">${value.date}</time>
</div>
<div class="article-post-card__title">
${value.title}
</div>
<div class="article-post-card__excerpt">
${value.introduction}
</div>
</div>
</a>
</div>`);
new_items.push($new_item[0]);
});
// Add the new items to the grid
$grid
.isotope('insert', new_items)
.imagesLoaded().progress(function () {
$grid.isotope('layout');
});
// Undo Button Disable
button.prop("disabled", false);
// Set Offset
var offset = button.data("offset");
button.data("offset", offset + 10);
// Script has run
has_run = true;
return false;
},
error: function (xhr, status, error) {
console.log("There was an error", error);
}
});
});
});
If you guys could critique I think this would be useful for others too.
I have been delving into AJAX within WordPress and have followed many tutorials on how to do so, but every single request returns a 400 error.
In my functions.php I have added the following:
Step 1 - Register and add scripts to WordPress
/**
* Initialize the JavaScript we need for loading more
*
* @return void
*/
function ajax_load_more_init()
{
// Register and enqueue the script we need for load more
wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
wp_enqueue_script('ajax-load-more-script');
// Localize the script so we can access tthe variables in PHP
wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
'ajax_url' => admin_url('admin-ajax.php'),
));
}
Step 2 - Enqueue the actual script
/**
* Add AJAX loader to scripts
*/
add_action('wp_enqueue_scripts', 'ajax_load_more_init');
Step 3 - Allow AJAX to be used on the frontend
// AJAX Hook for users
add_action('wp_ajax_ajax_load_more', 'ajax_load_more');
add_action('wp_ajax_nopriv_ajax_load_more', 'ajax_load_more');
Step 4 - A dead simple function for testing
/**
* The backend PHP to actually load more posts
*
* @return void
*/
function ajax_load_more()
{
echo "TESTING";
wp_die();
}
In my actual AJAX script I have the following:
jQuery(document).ready(function ($) {
// Initialize Isotope as $grid
const $grid = $('#grid').isotope({
itemSelector: '.grid-item',
percentagePosition: true,
animationEngine: 'best-available', //CSS3 if browser supports it, jQuery otherwise
animationOptions: {
duration: 800,
},
masonry: {
columnWidth: '.grid-item',
gutter: 30,
},
})
var has_run = false;
var init_offset = 0;
// Hook into click event
$('button.load-more-posts').click(function (e) {
e.preventDefault();
var button = $(this);
var nonce = $(this).data("nonce");
console.log('Nonce is: ' + nonce);
// Disable the button
button.prop("disabled", true);
// Check the offset
if (has_run == false) {
button.data('offset', $(this).data("offset"));
init_offset = $(this).data("offset");
}
console.log('Initial offset is: ' + init_offset);
console.log('Initial offset is: ' + button.data('offset'));
// Perform AJAX request
$.ajax({
type: 'POST',
dataType: 'json',
url: ajax_load_more_object.ajax_url,
contentType: 'application/json; charset=utf-8',
data: {
action: 'ajax_load_more',
security: nonce,
init_offset: init_offset,
offset: button.data('offset'),
},
beforeSend: function (xhr) {
console.log('Loading more posts...')
button.text('Loading');
},
success: function (response) {
console.log(response);
// Undo Button Disable
button.prop("disabled", false);
// Set Offset
button.data("offset", offset + 10);
// Script has run
has_run = true;
return false;
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
}
});
});
});
As you can see in the $ajax
call, the url is to admin-ajax
and the action is the trailing part of wp_ajax_nopriv_ajax_load_more
.
The nonce comes from the button on the front end like so:
<button class="load-more-posts" data-nonce="<?php echo wp_create_nonce('load_more_ajax'); ?>" data-offset="10">Load More Posts</button>
My working code
So, thanks to everyone's shared input I was able to come up with a solution that I'll detail below:
In functions.php (should really be in a plugin)
/**
* Additional thumbnail sizes
*/
add_theme_support('post-thumbnails');
add_image_size('post-thumbnail-square', 300, 300, true);
/**
* Initialize the JavaScript we need for loading more
*
* @return void
*/
function ajax_load_more_init()
{
// Register and enqueue the script we need for load more
wp_register_script('ajax-load-more-script', get_template_directory_uri() . '/assets/scripts/ajax-load-more.js', array('jquery'));
wp_enqueue_script('ajax-load-more-script');
// Localize the script so we can access tthe variables in PHP
wp_localize_script('ajax-load-more-script', 'ajax_load_more_object', array(
'ajax_url' => admin_url('admin-ajax.php'),
));
}
/**
* Add AJAX loader to scripts
*/
add_action('wp_enqueue_scripts', 'ajax_load_more_init');
/**
* Allow AJAX to be used on the front end
*/
add_action('wp_ajax_ajax_load_more_posts', 'ajax_load_more_posts_callback');
add_action('wp_ajax_nopriv_ajax_load_more_posts', 'ajax_load_more_posts_callback');
/**
* The backend PHP to actually load more posts
*
* @return void
*/
function ajax_load_more_posts_callback()
{
// First check the nonce, if it fails the function will break
check_ajax_referer('ajax_load_more_posts', 'security');
// Get the data we have from the load more button
$offset = $_POST['offset'];
$init_offset = $_POST['init_offset'];
// Get posts with given offset
if ($offset != null && absint($offset) && $init_offset != null && absint($init_offset)) {
// Finally, we'll set the query arguments and instantiate WP_Query
$args = array(
'post_type' => 'post',
'posts_per_page' => $init_offset,
'offset' => $offset
);
$post_list = array();
$query = new WP_Query($args);
if ($query->have_posts()) :
while ($query->have_posts()) : $query->the_post();
//$categories = get_the_categories();
$post_list[] = array(
'category' => get_the_category(),
'title' => get_the_title(),
'introduction' => get_field('introduction'),
'date' => get_the_date(),
'permalink' => get_permalink(),
'thumbnail' => get_the_post_thumbnail(),
);
endwhile;
endif;
echo json_encode($post_list);
wp_die();
}
}
In my ajax-loader script
jQuery(document).ready(function ($) {
var has_run = false;
var init_offset = 0;
// Hook into click event
$('button.load-more-posts').click(function (e) {
var $grid = $('#grid').isotope({
itemSelector: '.grid-item',
});
e.preventDefault();
var button = $(this);
var nonce = $(this).data("nonce");
// Disable the button
button.prop("disabled", true);
// Check the offset
if (has_run == false) {
button.data('offset', $(this).data("offset"));
init_offset = $(this).data("offset");
}
// Perform AJAX request
$.ajax({
type: 'POST',
dataType: 'json',
url: ajax_load_more_object.ajax_url,
data: {
action: 'ajax_load_more_posts',
security: nonce,
init_offset: init_offset,
offset: button.data('offset'),
},
beforeSend: function (xhr) {
console.log('Loading more posts...')
button.text('Loading');
},
success: function (response) {
console.log(response);
button.text('Load more');
// An array to store new items added via AJAX
var new_items = [];
// Run through JSON
$.each(response, function (key, value) {
var $new_item = $(`<div class="grid-item article-post-card ${value.category[0]['slug']}">
<a class="article-post-card__anchor" href=" ${value.permalink}" alt="${value.title}">
<div class="article-post-card__featured-image-container">
<div class="article-post-card__overlay"></div>
<div class="article-post-card__featured-image">
${value.thumbnail}
</div>
<div class="article-post-card__category-label">
${value.category[0]['name']}
</div>
</div>
<div class="article-post-card__content-wrapper">
<div class="article-post-card__publish-date">
<time class="updated" datetime="">${value.date}</time>
</div>
<div class="article-post-card__title">
${value.title}
</div>
<div class="article-post-card__excerpt">
${value.introduction}
</div>
</div>
</a>
</div>`);
new_items.push($new_item[0]);
});
// Add the new items to the grid
$grid
.isotope('insert', new_items)
.imagesLoaded().progress(function () {
$grid.isotope('layout');
});
// Undo Button Disable
button.prop("disabled", false);
// Set Offset
var offset = button.data("offset");
button.data("offset", offset + 10);
// Script has run
has_run = true;
return false;
},
error: function (xhr, status, error) {
console.log("There was an error", error);
}
});
});
});
If you guys could critique I think this would be useful for others too.
I have not tested your code, but one problem I noticed is the contentType
property in your $.ajax()
call:
contentType: 'application/json; charset=utf-8',
because that way, (from the PHP side) the action (i.e. ajax_load_more
) is not available in $_REQUEST['action']
which WordPress uses to determine the AJAX action being called, and when the action is not known, WordPress throws the error 400 Bad Request
, which in your case translates to "unknown AJAX action".
You should just omit the contentType
property, or don't set it to a JSON content type string.
Also, although you're just testing in ajax_load_more()
, make sure to return a proper JSON response/string because your dataType
is json
.
I hope this helps you. :)
From the jQuery.ajax()
documentation:
dataType
(default: Intelligent Guess (xml, json, script, or html)
)
The type of data that you're expecting back from the server. If none is specified, jQuery will try to infer it based on the MIME type of the response
contentType
(default: 'application/x-www-form-urlencoded; charset=UTF-8'
)
When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. If you explicitly pass in a content-type to
$.ajax()
, then it is always sent to the server (even if no data is sent).
So dataType
is the content type (MIME) of the content from the AJAX response, whereas contentType
is the content type of the content you are sending to the PHP/server-side function (which is ajax_load_more()
in your case).
So by using the wrong content type the headers become unreadable?
No it is not about the headers become unreadable - (request) headers are always readable in PHP. It's just that request payload (in JSON) is not supported by the WordPress AJAX - only the request body. But you can of course, send form field data/value as JSON - for example, myField: '{"foo":"bar","baz":1}'
. Just don't set the content type header to JSON.
But if you really must send a JSON request payload instead of a standard URL-encoded form data (i.e. the request body), then you can append the AJAX action to the AJAX URL - e.g. /wp-admin/admin-ajax.php?action=ajax_load_more
. That way, you wouldn't get the error 400
; however, it's up to you to retrieve/parse the JSON request payload..
jQuery.ajax({
type: 'POST',
url: '/wp-admin/admin-ajax.php?action=ajax_load_more',
contentType: 'application/json; charset=utf-8',
data: {
action: 'ajax_load_more',
foo: 'bar',
}
});
jQuery.ajax({
type: 'POST',
url: '/wp-admin/admin-ajax.php',
data: {
action: 'ajax_load_more',
foo: 'bar',
}
});
You haven't specify any response, functions that handle the ajax requests must return a json encoded array using wp_send_json _success() built-in function or other similar functions.
Exemple :
function ajax_load_more(){
wp_send_json_success('testing...');
//Don t nee to use die() wp_send_json success will send json and die
}
you can find out how these functions work in the wordpress codex, the php function can't echo anything, php send data to jquery and jquery then can echo data to html.
action=ajax_load_more_posts&security=25a70fd9b9&init_offset=10&offset=10
– Jesse Orange Commented Mar 12, 2019 at 12:32