We have a plugin that allows us to manage custom post types and we'd like to add AJAX functionality in saving, editing, and deleting posts. I couldn't find similar problems in the internet so I'm wondering if this isn't something easily doable?


You can technically make a XHR to post.php via JavaScript. Below is a proof-of-concept for saving/editing posts only. I haven't tested it at all, so I'm sure you will need to tweak it. I wanted to give you a basic workflow for accomplishing something like this. You can take it and run with it if you need to extend it.

Step 1: Add AJAX handler to admin_head for new and existing posts.

function my_post_type_xhr(){
    global $post;
    if('my_post_type' === $post->post_type){
        $post_url = admin_url('post.php'); #In case we're on post-new.php
        echo "
                //Click handler - you might have to bind this click event another way
                $('input#publish, input#save-post').click(function(){
                    //Post to post.php
                    var postURL = '$post_url';

                    //Collate all post form data
                    var data = $('form#post').serializeArray();

                    //Set a trigger for our save_post action
                    data.push({foo_doing_ajax: true});

                    //The XHR Goodness
                    $.post(postURL, data, function(response){
                        var obj = $.parseJSON(response);
                            alert('Successfully saved post!');
                            alert('Something went wrong. ' + response);
                    return false;
add_action('admin_head-post.php', 'my_post_type_xhr');
add_action('admin_head-post-new.php', 'my_post_type_xhr');

Step 2: Hook into the save_post action.

This runs after the post has been saved to the database, so you can save whatever postmeta you need and halt the page render.

add_action('save_post', 'save_my_post_type');
function save_my_post_type($post_id){
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;

    #If this is your post type
    if('my_post_type' === $_POST['post_type']){
        //Save any post meta here

        #We conditionally exit so we don't return the full wp-admin load if foo_doing_ajax is true
        if(isset($_POST['foo_doing_ajax']) && $_POST['foo_doing_ajax'] === true){
            header('Content-type: application/json');
            #Send a response
            echo json_encode(array('success' => true));
            #You should keep this conditional to degrade gracefully for no JS

The only major caveat I can see is that you will need to refresh nonces on the page somehow via XHR.

For deleting a post, I'm not sure why you would want to use AJAX when it's a simple click of a link that does the job already. Deleting a page in every case will send you to another page, namely, edit.php. This defeats the purpose of using AJAX which is to keep everything asynchronous on one page load.

Hope this helps you out.

  • FYI: This solution works, but has two bugs. data.push should have two entries, with keys "name" and "value". Also, checking the $_POST value, we don't need to compare the type. $_POST['foo_doing_ajax'] == TRUE is sufficient, and won't fail in the event php returns "true" as a string (which it does as written). I tweaked your code a bit and posted it as a solution below. Thanks! – Jess Mann Apr 12 '17 at 22:30

Brian Fegter's response was the right idea, but it had a few bugs. Here's a more polished solution based on the same principle.

Step 1: PHP Logic.

Place in functions.php (or a plugin file)

// Saving the post via AJAX
add_action('save_post', 'save_post_ajax');
function save_post_ajax( $post_id )
        # Ignore autosaves
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)

        # Only enabled for one post type
        # Remove this if statement if you want to enable for all post types
        if ($_POST['post_type'] == 'my_custom_post_type')
                # Send JSON response
                # NOTE: We use ==, not ===, because the value may be String("true")
                if (isset($_POST['save_post_ajax']) && $_POST['save_post_ajax'] == TRUE)
                        header('Content-type: application/json');
                        echo json_encode(array('success' => true));

                        # Don't return full wp-admin

Step 2: Create javascript in an external file

Brian's solution of echoing out the js directly on to the page works too.

// Avoid collisions with other libraries
(function($) {
        // Make sure the document is ready
        $(document).ready(function() {
                // This is the post.php url we localized (via php) above
                var url = ajax_object.post_url;
                // Serialize form data
                var data = $('form#post').serializeArray();
                // Tell PHP what we're doing
                // NOTE: "name" and "value" are the array keys. This is important. I use int(1) for the value to make sure we don't get a string server-side.
                data.push({name: 'save_post_ajax', value: 1});

                // Replaces wp.autosave.initialCompareString
                var ajax_updated = false;

                 * Supercede the WP beforeunload function to remove
                 * the confirm dialog when leaving the page (if we saved via ajax)
                 * The following line of code SHOULD work in $.post.done(), but 
                 *     for some reason, wp.autosave.initialCompareString isn't changed 
                 *     when called from wp-includes/js/autosave.js
                 * wp.autosave.initialCompareString = wp.autosave.getCompareString();
                $(window).on( 'beforeunload.edit-post', function() {
                        var editor = typeof tinymce !== 'undefined' && tinymce.get('content');

                        // Use our "ajax_updated" var instead of wp.autosave.initialCompareString
                        if ( ( editor && !editor.isHidden() && editor.isDirty() ) ||
                                ( wp.autosave && wp.autosave.getCompareString() != ajax_updated) ) { 
                                return postL10n.saveAlert;

                // Post it
                $.post(url, data, function(response) {
                        // Validate response
                        if (response.success) {
                                // Mark TinyMCE as saved
                                if (typeof tinyMCE !== 'undefined') {
                                        for (id in tinyMCE.editors) {
                                                var editor = tinyMCE.get(id);
                                                editor.isNotDirty = true;
                                // Update the saved content for the beforeunload check
                                ajax_updated = wp.autosave.getCompareString();

                                console.log('Saved post successfully');
                        } else {
                                console.log('ERROR: Server returned false. ',response);
                }).fail(function(response) {
                        console.log('ERROR: Could not contact server. ',response);

Step 3: Enqueue your javascript file

If you echoed it out (like Brian), you don't have to do this. I prefer this method because it allows us to dequeue the script, localize variables, and adjust script load order easily.

function my_post_type_xhr()
        global $post;
        # Only for one post type. 
        if ($post->post_type == 'custom_post_type')
                # The url for the js file we created above
                $url = '/url/to/my/javascript.js';

                # Register and enqueue the script, dependent on jquery
                wp_register_script( 'my_script', $url, array('jquery') );
                wp_enqueue_script( 'my_script' );

                # Localize our variables for use in our js script
                wp_localize_script( 'my_script', 'ajax_object', array(
                                'post_id' => $post_id,
                                'post_url' => admin_url('post.php'),
                ) );

add_action('admin_head-post.php', 'my_post_type_xhr');
add_action('admin_head-post-new.php', 'my_post_type_xhr');

This snippet does not address nonces. Anyway, I hope that helps someone.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.