Is there an easy way to AJAX-ify saving of post?

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?

Related posts

Leave a Reply

3 comments

  1. 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 "
            <script>
                jQuery(document).ready(function($){
                    //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);
                            if(obj.success)
                                alert('Successfully saved post!');
                            else
                                alert('Something went wrong. ' + response);
                        });
                        return false;
                    });
                });
            </script>";
        }
    }
    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));
                exit;
                #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.

  2. 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)
                    return;
    
            # 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
                            exit;
                    }
            }
    }
    

    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).unbind('beforeunload.edit-post');
                    $(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);
                    });
            });     
    })(jQuery);
    

    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.

  3. Here’s a more polished solution 😉

    • Fix TinyMce error
    • Add form submit event listener.
    • Hide spinner after complete request and enable save button again.

    Also i make it shorter for easy to use 🙂 just put this code in your functions.php:

    <?php
    
    // Saving the post via AJAX
    add_action( 'save_post', function ( $post_id ) {
        # Ignore autosaves
        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
            return;
        }
    
        # Only enabled for one post type
        # Remove this if statement if you want to enable for all post types
        if ( $_POST['post_type'] === 'post' ) {
            # 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 ) {
                wp_send_json_success();
            }
        }
    } );
    
    
    function my_post_type_xhr() {
        # Only for one post type.
        if ( get_post_type() === 'post' ) {
    
            ?>
            <script>
                // Avoid collisions with other libraries
                (function ($) {
                    // Make sure the document is ready
                    $(document).ready(function () {
    
                        $(document).on('submit', 'form#post', function (e) {
                            e.preventDefault()
    
                            // This is the post.php url we localized (via php) above
                            var url = '<?= admin_url( 'post.php' ) ?>'
                            // 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).unbind('beforeunload.edit-post')
                            $(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) {
                                            if (tinyMCE.get(id))
                                                tinyMCE.get(id).setDirty(false)
                                        }
                                    }
                                    // 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)
                            }).done(function (){
                                if ( wp.autosave ) {
                                    wp.autosave.enableButtons();
                                }
    
                                $( '#publishing-action .spinner' ).removeClass( 'is-active' );
                            })
    
                            return false
                        })
                    })
                })(jQuery)
            </script>
            <?php
        }
    }
    
    add_action( 'admin_footer-post.php', 'my_post_type_xhr', 999 );
    add_action( 'admin_footer-post-new.php', 'my_post_type_xhr', 999 );