Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hook for successful post save #17632

Closed
kevinfodness opened this issue Sep 27, 2019 · 39 comments
Closed

Add hook for successful post save #17632

kevinfodness opened this issue Sep 27, 2019 · 39 comments
Labels
[Status] Duplicate Used to indicate that a current issue matches an existing one and can be closed

Comments

@kevinfodness
Copy link

Is your feature request related to a problem? Please describe.
There is no concise way in Gutenberg to fire a JavaScript callback when a post save operation is finished. You can cobble something together using wp.data.subscribe, but you have to keep track of state yourself, especially when processing updates to an already published post.

Describe the solution you'd like
It would be helpful to have a hook available to call a user-specified function when a save operation successfully concludes, especially one that is able to fire after both the REST request and the POST operation for metaboxes (if present) have concluded. This could be at least a partial solution to #12903 in that developers could take an action in Gutenberg with the full post object after all save operations have concluded, including sending an additional REST request to perform actions for plugins, like indexing a post in Elasticsearch or publishing/updating it in Apple News.

Describe alternatives you've considered
Using wp.data.subscribe, but this requires that you manage state yourself so that the callback fires once and only once after the post has fully and successfully saved. The subscribe callback ends up getting called a lot and nearly all of those calls can be ignored. Also, processing data server-side using a save_post callback, but this is susceptible to the problems outlined in #12903. Especially for developers that are looking for a simple way to say "give me the state of a post after it has saved to the remote," implementation via a hook would be ideal.

@adamsilverstein
Copy link
Member

Hi @kevinfodness thanks for opening this. This looks like a duplicate of #13413, can we consolidate the discussion there?

@swissspidy swissspidy added the [Status] Duplicate Used to indicate that a current issue matches an existing one and can be closed label Oct 1, 2019
@swissspidy
Copy link
Member

The same has been reported in #15568, which was also closed as a duplicate of #13413. Let's focus on that one 👍

@mboynes
Copy link
Contributor

mboynes commented Oct 2, 2019

@swissspidy @adamsilverstein I think this should stay open as a separate issue. #13413 is about pre-save validation, which is distinctly separate from a post-save event (action).

@mboynes
Copy link
Contributor

mboynes commented Oct 2, 2019

I should also note that this is different from #15568 as well, which is also about validation.

@beta2k
Copy link

beta2k commented Jan 2, 2020

any update on this?

@WPprodigy
Copy link
Contributor

Chiming in that this does feel quite different than the issue it was closed for. wp.data.subscribe() isn't actually called (at least not in the latest version of Gutenberg) once the saving is completed.

@adamsilverstein
Copy link
Member

@WPprodigy Good point, this is a slightly different use case, however I would expect to still be able to handle this using subscribe. I'm going to dig a bit further to see if it possible currently. I will report back here once I try.

@adamsilverstein
Copy link
Member

@WPprodigy

Can you give something like this a try? The snippet leverages isSavingPost. when we see this event start we set checked to false. when we see it not true and checked is false, we check again.

In this way you can use subscribe like a triggered action, although that is separate from the issue @kevinfodness described here, which I understand as requesting that Gutenberg fire a specific (single) action (eg. wp.hooks.doAction) after the save to prevent having to use subscribe at all. If does feel like a common enough pattern that we should consider ways to make it easier for developers to tap into.

import { select, subscribe } from '@wordpress/data';
const { isSavingPost } = select( 'core/editor' );

var checked = true; // Start in a checked state.
subscribe( () => {
    if ( isSavingPost() ) {
		 checked = false;
    } else {
 		if ( ! checked ) {
            checkPostAfterSave(); // Perform your custom handling here.
            checked = true;
        }

    }
} );

@youknowriad
Copy link
Contributor

The solution proposed by @adamsilverstein is a good one for post save behavior. That said, this has come up a few times and it was proposed lately on #core-js chats to make this code easier by introducing a subscribeOnChange API to the data package.

wp.data.subscribeOnChange( () => wp.data.select( 'core/editor' ).isSavingPost(), ( isSaving ) => {
  if ( ! isSaving )  {
    // saving is finished do something
  }
} )

I think we should open a new dedicate issue to track this addition. I'm still not sure whether this belongs to the data package or compose, or something else, since it's just a small JS utility with no dependency.

@mboynes
Copy link
Contributor

mboynes commented Apr 15, 2020

@youknowriad @adamsilverstein for my own edification, what's the argument against firing a wp.hooks.doAction? Are there performance concerns, is the Gutenberg team trying to avoid using wp.hooks, ... something else? Since that would prevent developers from having to use subscribe(), it seems like a significantly more performant option to me from the extensibility side.

@youknowriad
Copy link
Contributor

Are there performance concerns, is the Gutenberg team trying to avoid using wp.hooks

yes, we try to avoid hooks because we found that this is not the best abstraction for client side applications (there's some discussions in a previous #core-js meeting). The hooks API is not reactive, it's more suited for single threaded applications, while JavaScript is async by nature, using hooks will quickly result in buggy code that is hard to maintain or make sense of.

We had some specific examples in the past where we used hooks and decided to rollback to more specific APIs (block style variations come to my mind).

in terms of extensibility, the data API is the central layer for client side code, as it has selectors to retrieve the data (state), actions to mutate the data and subscribe to be informed about data change.

The performance argument is a good one but the unique subscribe and global state is at the root of the Redux architecture, and performance is modeled around immutability, which means objects and arrays don't mutate their content (never), instead, we generate new instances each time a change is required, this makes it easier to use strict equality (performant) and prevent the downsides of a unique subscribe. All these patters work well tied to React components where you could use useSelect that takes care of the subscriptions for you while ensuring performant code.

@Flynsarmy
Copy link

Flynsarmy commented Sep 25, 2020

When hitting update button, the checkPostAfterSave() from #17632 (comment) will occur before the server processes the update POST request, and before the 'Updating...' button changes back to 'Update'. This is still too early and not what I need. I'm trying to retrieve data from a REST API endpoint after each post update (ie after the Updating... button turns back to Update and the POST request has returned its 200 response) but it just doesn't seem possible with Gutenberg at this stage.

@swissspidy should not have closed this issue. It's not a duplicate of the one he marked.

@swissspidy
Copy link
Member

It‘s still holding true that this should be done using the subscribe functions

@Flynsarmy
Copy link

Flynsarmy commented Sep 25, 2020

There is no way for subscribe to know that an update has completed. The closest we get is with the code snippet provided here which happens before the POST request has returned or even executed on the server.

I can't do an apiFetch() in every single subscribe call because it's called 50 times after that happens as well as during and before and also when we're not updating.

There is still not a single way anyone has been able to run an action a single time after a post update completes therefore what you're saying is not true. subscribe() can't do what we need it to do.

@adamsilverstein
Copy link
Member

adamsilverstein commented Oct 19, 2020

before the 'Updating...' button changes back to 'Update'.

@Flynsarmy Are you sure about this? "isSavingPost" is the exact check the button uses to determine state and thus what the button reads - https://github.com/WordPress/gutenberg/blob/master/packages/editor/src/components/post-publish-button/index.js#L212.

I can't do an apiFetch() in every single subscribe call because it's called 50 times after that happens as well as during and before and also when we're not updating.

What are you trying to fetch?

You should only call fetch once, when saving happens then is no longer happening. the subscribe code only checks if the saving state has changed.

There is still not a single way anyone has been able to run an action a single time after a post update completes

This is exactly what the linked code describes how to do:

  1. set a variable "isSaving" to false
  2. Subscribe to events
  3. wait (many events) for saving to start (one event). isSaving is now true.
  4. wait for saving to stop (one event)
  5. perform your event handling once. saving has completed, the store will contain the latest post data.

If you need to wait for the next save event, start over at step 1.

That said, this has come up a few times and it was proposed lately on #core-js chats to make this code easier by introducing a subscribeOnChange API to the data package.

@youknowriad is this still something you want to add? Shall I create a follow up issue?

@outcomer
Copy link

@adamsilverstein just imagine what will be if in checkPostAfterSave() you will call for example wp createNotice() trying to say something to user....

@Flynsarmy
Copy link

Looks like I was wrong. The example code provided by adam here works a charm - it'll run once after the update AJAX request has received its response from the server.

Just so there is more sample code on the internet, here's a very dumbed down component I wrote that I've confirmed to work. A very simple component that just alerts the post fwpaps_datetime meta value after an update (This meta value is set server-side during the update.

import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';

const AfterPostSavePanel = ( { isSavingPost, newMetaValue } ) => {
	const [ checked, setChecked ] = useState( true );

	if ( isSavingPost && checked ) {
		setChecked( false );
	} else if ( ! isSavingPost && ! checked ) {
		// eslint-disable-next-line no-alert,no-undef
		alert( `Updated meta value taken from server: ${ newMetaValue }?` );
		setChecked( true );
	}

	return <></>;
};

export default compose( [
	withSelect( ( select ) => {
		const { getEditedPostAttribute, isSavingPost } = select(
			'core/editor'
		);

		return {
			isSavingPost: isSavingPost(),
			newMetaValue: getEditedPostAttribute( 'meta' ).fwpaps_datetime,
		};
	} ),
] )( AfterPostSavePanel );

Here's the server side code:

/**
 * Fires when preparing to serve an API request.
 *
 * @param WP_REST_Server $wp_rest_server
 * @return WP_REST_Server
 */
add_action('rest_api_init', function (WP_REST_Server $wp_rest_server) {
    register_post_meta('post', 'fwpaps_datetime', [
        'show_in_rest' => true,
        'single' => true,
        'type' => 'string',
    ]);

    return $wp_rest_server;
});

/**
 * Save Post Action - Deprecated with Gutenberg.
 *
 * @param int $post_ID
 * @param WP_Post $post
 * @param bool $update
 * @return mixed
 */
add_action('save_post', function (int $post_ID, WP_Post $post, bool $update) {
    if ((defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || empty($post_ID)) {
        return false;
    }

    update_post_meta(
        $post_ID,
        'fwpaps_datetime',
        current_time('r')
    );
}, 10, 3);

@outcomer
Copy link

@Flynsarmy Thanks for suggested solution. But in generally i was trying to say another thing. All the suggested solutions work fine until you start using WP itself for some action. For example, try replacing your alert( "Updated meta value taken from server: ${ newMetaValue }?" ); with WP createNotice( "Updated meta value taken from server ...." ); and you will get into infinite loop.
Cause when you do subscribe with WP in fact you will get all events that lead or could lead to state tree changing. And once you understood that post started to save or saved (does not matter) and called createNotice() - you change the tree and get new event into suscriber.
It can be overcame with more complex logic inside suscriber (two-level logic). But why??? While everybody talks about better abstraction and avoid using hooks we have to do something like this just to get ability to get some simple events.
If hooks are so unlovely then ok, but why not to include into Editor source code regular native JS events broadcasting?
Who will be worse off from this if somewhere in the editor's code they write
document.dispatchEvent ( new Event ('savePostStart', { bubbles: false }) );
Noone framework notification system will ever be faster than pure JS code. In addition, React is not eternal. JS is also not eternal, but there is reason to believe that it will live longer then React.

This monster construction require just only for triggering two simple JS native events and being idependant on tree state changes.

function blabla () {

	var flagOne = true;
	var flagTwo = false;
	const unSubscribeOne = wp.data.subscribe(() => {
		var isSavingPost = wp.data.select("core/editor").isSavingPost();
		var isAutosavingPost = wp.data.select("core/editor").isAutosavingPost();

		if (isSavingPost && flagOne && !isAutosavingPost) {
			flagOne = false;
			flagTwo = true;
			document.dispatchEvent(
				new Event('savePostStart', {
					bubbles: false,
				})
			);
			const unSubscribeTwo = wp.data.subscribe(() => {
				var isSavingPost = wp.data.select('core/editor').isSavingPost();
				var isAutosavingPost = wp.data.select('core/editor').isAutosavingPost();
		
				if (!isSavingPost && flagTwo && !isAutosavingPost) {
					flagOne = true;
					flagTwo = false;
					document.dispatchEvent(
						new Event('savePostFinish', {
							bubbles: false,
						})
					);
				}
			});
		}
	});
}

@adamsilverstein
Copy link
Member

@Flynsarmy glad to hear you were able to get it working!

@outcomer I feel like you are probably right: this should be easier for developers. Maybe we can create a simpler event subscription API that would let you register a callback on an event completion? (what do you think @youknowriad?)

Internally this would look like my sample code, but as a developer you could use a simpler API to request the notification. I'm not sure what the ideal API looks like, hopefully something developers coming from event/listener driven development can quickly understand.

@adamsilverstein just imagine what will be if in checkPostAfterSave() you will call for example wp createNotice() trying to say something to user....

This is not an issue because by the time you would call createNotice you are no longer subscribing: the completion of save already occurred.

I'm a big fan of hooks and event driven programming as well, however Gutenberg chose a different approach that is more purely state driven, in the redux/react way. This new approach offers some distinct advantages in complex systems, so it is worth considering and learning more about.

@outcomer
Copy link

outcomer commented Dec 22, 2020

@adamsilverstein Thanks! I'm glad I was heard))

This is not an issue because by the time you would call createNotice you are no longer subscribing: the completion of save already occurred.

I don't think I can be considered a great front-end specialist and maybe I am missing something in your solution. But as I understand it - it will only work only once from the moment the page is loaded. Namely:

  • As soon as you unsubscribe from events, you can display a message to the user using createNotice(). It is true. But if i click "Save Post" again you will not get this even, cause you have unsubscribed. No? So to deal with this problem you have to: Subscribe->Catch your event->Unsubscribe->ShowNotice-> and subscribe back. But how you will do it not knowing that post fully saved?

And a couple of words about the events API. It makes sense to see how this is done in Symphony, so as not to reinvent the wheel. It has both EventListner and EventSubscriber. And it does not throw any abstract event that something going on and changing or going to change. So you have a chance to catch separate event once or subscribe to it forever - it is up to you. Or create your own event and do the same.

@csalmeida
Copy link

csalmeida commented Mar 31, 2021

After trying some of the awesome suggestions available in this thread I was still finding challenging to trigger JS logic when the post is actually done saving, including after hooks such as post_updated had run. I've found a way using MutationObserver so if you would like to get to it please scroll to the end of my comment.

In my case I'm updating a metabox section via Javascript and the fact that the update occurs before all the data saving is completely done sometimes returns a state with data that does not represent what actually is stored.

Considering the following example where there's a callback running on the post_updated hook. An artificial delay as been added to make sure JavaScript was running after this function was done:

function example_run_on_save_callback( $post_id ) {
  # Delay the save for 5 seconds in order to make sure Javascript runs at the correct time.
  sleep(5);
}

add_action( 'post_updated', 'example_run_on_save_callback' );

Clicking the publish/update button on a post edit page will result in the following logs:

[30-Mar-2021 13:49:17 UTC] 1617112157
[30-Mar-2021 13:49:17 UTC] Started example_run_on_save_callback() execution
[30-Mar-2021 13:49:22 UTC] 1617112162
[30-Mar-2021 13:49:22 UTC] Ended example_run_on_save_callback() execution
[30-Mar-2021 13:49:23 UTC] 1617112163
[30-Mar-2021 13:49:23 UTC] Started example_run_on_save_callback() execution
[30-Mar-2021 13:49:28 UTC] 1617112168
[30-Mar-2021 13:49:28 UTC] Ended example_run_on_save_callback() execution

Of one single action post_update has ran twice but the code does take 5 seconds to run. The fact that some hooks might run more than once is known (I think I read this somewhere at some point too but lost the source, sorry). This particular hook runs twice because it does run at two instances it seems:

    Priority: 10
        wp_save_post_revision
    Priority: 12
        wp_check_for_changed_slugs

Visually this is conveyed as if the Publish/Update button is in a "progress" animation for around 5 seconds, then the button will appear to be in a "unavailable\not interactable" state for another 5 seconds which matches the two times this function runs.

Wordpress update button in various states during save

The image above is a .gif and should show the various states of the update button.

On the Javascript side there are two different snippets I've tried to run code on post save, one from @adamsilverstein (available here) and another from @davidfcarr (available here). Both have the same goal of running JavaScript when the post is done saving:

// Adam's Example
// https://github.com/WordPress/gutenberg/issues/17632#issuecomment-583772895
const { isSavingPost } = wp.data.select( 'core/editor' );

var checked = true; // Start in a checked state.
wp.data.subscribe( () => {
    if ( isSavingPost() ) {
     checked = false;
    } else {
     if ( ! checked ) {
        // Adding a log to understand when this runs.
        let currentDate = new Date();
        let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
        console.log(`
        ${time}
        Post is done saving via isSavingPost`);

        checked = true;
        }
    }
} );

// David's Example
// https://github.com/WordPress/gutenberg/issues/5975#issuecomment-483488988
let wasSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
let wasAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
let wasPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();

wp.data.subscribe( () => {
  
  const isSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
  const isAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
  const isPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();

  // Trigger on save completion, except for autosaves that are not a post preview.
  const isDoneSaving = (
  ( wasSavingPost && ! isSavingPost && ! wasAutosavingPost ) ||
  ( wasAutosavingPost && wasPreviewingPost && ! isPreviewingPost )
  );

  // Save current state for next inspection.
  wasSavingPost = isSavingPost;
  wasAutosavingPost = isAutosavingPost;
  wasPreviewingPost = isPreviewingPost;

  if ( isDoneSaving ) {  
    // Adding a log to understand when this runs.
    let currentDate = new Date();
    let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
    console.log(`
    ${time}
    Post is done saving via isDoneSaving`);
  }
});

Both will result on the code inside the conditional isSavingPost and isDoneSaving running once each. This is important because that means the JS code won't be executed the same number of times as post_updated:

13:49:22
Post is done saving via isSavingPost

13:49:22
Post is done saving via isDoneSaving

When comparing the logs, the JS script got triggered at the correct time as the first run of the PHP example callback attached to post_updated:

[30-Mar-2021 13:49:17 UTC] 1617112157
[30-Mar-2021 13:49:17 UTC] Started example_run_on_save_callback() execution
[30-Mar-2021 13:49:22 UTC] 1617112162
[30-Mar-2021 13:49:22 UTC] Ended example_run_on_save_callback() execution

13:49:22
Post is done saving via isSavingPost
13:49:22
Post is done saving via isDoneSaving

[30-Mar-2021 13:49:23 UTC] 1617112163
[30-Mar-2021 13:49:23 UTC] Started example_run_on_save_callback() execution
[30-Mar-2021 13:49:28 UTC] 1617112168
[30-Mar-2021 13:49:28 UTC] Ended example_run_on_save_callback() execution

No entries in the JS console.

In my case, a metabox section is updated via Javascript and the fact that isSavingPost occurs before the all the post saving hooks are done running can result in sometimes a state being returned with data that does not represent what actually is stored.

To run the Javascript code at the time publishing/updating was done, I've used MutationObserver. It's not using any subscriptions to Gutenberg's events so it might not be ideal but it does run the code at the right time:

// Mutation Observer requires a a new instance to be set.
// A function if passed as an argument to understand what to do with any detected mutations.
const observer = new MutationObserver(mutations => {
  // There could be one or more mutations so one way to access them is with a loop.
  mutations.forEach(record => {
    // In this case if the type of mutation is of attribute run the following block.
    // A mutation could have several types.
    if(record.type === 'attributes') {
       // Check if the button is-busy class has changed and if is disabled.
      const isNotBusy = !record.target.classList.contains('is-busy');
      const isInteractable = record.target.getAttribute('aria-disabled') === "true" ? false : true;
      
      // The button not being busy and being interactable means that
      // the publishing/updating is done for good.
      if (isNotBusy && isInteractable) {
       
       /**
         * Add your code here
       */  
    
        let currentDate = new Date();
        let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
        console.log(`
        ${time}
        Post is done saving via MutationObserver is done.`);
      }
    }
  });
});

// The publish button is used as a target to check for mutations in attributes.
const publishButton = document.querySelector('.editor-post-publish-button');

// Checks mutations in the buttons's attributes only.
observer.observe(publishButton, {
  attributes: true
});

When compared to the other methods I've mentioned, using mutation observer will run only once the publishing/updating is done:

11:33:32
Post is done saving via isSavingPost
11:33:32
Post is done saving via isDoneSaving
11:33:37
Post is done saving via MutationObserver is done.

The caveat is that if the Publishing/Update button ever changes the way it operates when saving a post it will break this script.

@swissspidy
Copy link
Member

@csalmeida Any HTML class names are just representations of the underlying React state, which is the source of truth. You should build your application around the React state, not its HTML representation (which can change).

For example, the is-busy class on the publish button is added whenever isSaving is true:

isBusy: ! isAutoSaving && isSaving && isPublished,

Similarly, aria-disabled is true when hasNonPostEntityChanges() is true:

'aria-disabled': isButtonDisabled && ! hasNonPostEntityChanges,

So what you are really missing in the examples you've shared is the check for hasNonPostEntityChanges() to get that same behavior.

There's no reason for using MutationObserver for this.

@csalmeida
Copy link

csalmeida commented Mar 31, 2021

Thank you! I do think my solution is not that good if the editor ever changes its markup it will just break. I appreciate it that you took the time to let me know how to do it the right way! 🙏

The file helps a lot however, I've noticed not all values used in the checks for is-busy and aria-disabled can be retrieved from wp.data.select( 'core/editor' ). I wonder if you could let me know how I can access the ones I'm missing?

One of them is isPostSavingLocked() which I have found and was able to access this way:

export function isPostSavingLocked( state ) {

let editorStore = wp.editor.store.instantiate();
editorStore.selectors.isPostSavingLocked();

I can't seem to access hasNonPostEntityChanges(), also available in the same file mentioned above and used in checks in the file you mentioned earlier:

export const hasNonPostEntityChanges = createRegistrySelector(

editorStore.selectors.hasNonPostEntityChanges();

I get the following error, what would be the best way to get this value?

Uncaught TypeError: (intermediate value).registry is undefined
    t https://wordpress.test/wp-includes/js/dist/data.min.js?ver=943087ae96d075f126df689839bb96b9:2
    v https://wordpress.test/wp-includes/js/dist/data.min.js?ver=943087ae96d075f126df689839bb96b9:2
    r https://wordpress.test/wp-includes/js/dist/data.min.js?ver=943087ae96d075f126df689839bb96b9:2
    <anonymous> debugger eval code:1

I think the method expects the global application state to be passed to it?

Apologies if this is obvious, just can't seem to work it out so far!

@swissspidy
Copy link
Member

Just wp.data.select('core/editor').hasNonPostEntityChanges() and wp.data.select('core/editor').isPostSavingLocked()

No instantiate() or anything needed.

@csalmeida
Copy link

csalmeida commented Apr 14, 2021

Thanks, I was able to access the values that way. However, it still does not do it for me so I had to stick with my solution for now until I have the time to study Gutenberg's source better.

The following script does solves the "how to run custom functionality on post save?" question in this issue:

/**
 * Consults values to determine whether the editor is busy saving a post.
 * Includes checks on whether the save button is busy.
 * 
 * @returns {boolean} Whether the editor is on a busy save state.
 */
function isSavingPost() {

  // State data necessary to establish if a save is occuring.
  const isSaving = wp.data.select('core/editor').isSavingPost() || wp.data.select('core/editor').isAutosavingPost();
  const isSaveable = wp.data.select('core/editor').isEditedPostSaveable();
  const isPostSavingLocked = wp.data.select('core/editor').isPostSavingLocked();
  const hasNonPostEntityChanges = wp.data.select('core/editor').hasNonPostEntityChanges();
  const isAutoSaving = wp.data.select('core/editor').isAutosavingPost();
  const isButtonDisabled = isSaving || !isSaveable || isPostSavingLocked;

  // Reduces state into checking whether the post is saving and that the save button is disabled.
  const isBusy = !isAutoSaving && isSaving;
  const isNotInteractable = isButtonDisabled && ! hasNonPostEntityChanges;
  
  return isBusy && isNotInteractable;
}

// Current saving state. isSavingPost is defined above.
var wasSaving = isSavingPost();

wp.data.subscribe( () => {

  // New saving state
  let isSaving = isSavingPost();

  // It is done saving if it was saving and it no longer is.
  let isDoneSaving = wasSaving && !isSaving;

  // Update value for next use.
  wasSaving = isSaving;


  if ( isDoneSaving ) {
    /**
     * Add additional functionality here.
     */

    // Adding a log to understand when this runs.
    let currentDate = new Date();
    let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
    
    console.log(`
    ${time}
    Post is done saving via isDoneSaving`);
  } // End of isDoneSaving

}); // End of wp.data.subscribe

However, that's not quite what I'm looking for since this script completes running on a post save and not when all the saving callbacks are done, this can be observed when comparing the logs of the execution of the callback against the JS meant to run on save completion:


# Editor is busy saving for the first time (update button animates)
[14-Apr-2021 09:06:39 UTC] Started example_run_on_save_callback() execution
[14-Apr-2021 09:06:44 UTC] Ended example_run_on_save_callback() execution

# This is the JS running ⚙️
09:6:44
Post is done saving via isDoneSaving

# Editor is busy doing something else (update button is in a disabled state with no animation)
[14-Apr-2021 09:06:45 UTC] Started example_run_on_save_callback() execution
[14-Apr-2021 09:06:50 UTC] Ended example_run_on_save_callback() execution

# I would like to run the JS here instead

Here's the button the states I mention above for reference:
Various states of the update button on the Gutenberg editor in a save cycle

I am probably either thinking about this wrong way all together and just can't work it out but I believe that the previous examples (please ignore the MutationObserver approach) all work fine to run something on save but that might mean that there are callbacks still running.

Either way it seems that this is probably not the correct thread for me to comment on since what I find challenging is the fact that Gutenberg seems to send a request to create the post and another to save metabox data and the post is being marked as saved on the first request which is correct but just not useful to what I'm trying to do.

Appreciate all your help! 🙏

@easin-xs
Copy link

easin-xs commented Jan 9, 2022

Need js hook after post save. This will be more useful for us. 1+

@henryobiaraije
Copy link

@easin-xs
I agree with you. A few js hooks can still be added (especially in a case like this) to solve this.
@csalmeida 's solution is what I use currently but still doesn't fix this completely.

@rchiba-cafemedia
Copy link

rchiba-cafemedia commented Mar 29, 2022

I agree with @easin-xs and @MachinePereere that having a post-save hook would be incredibly convenient

@wpsoul
Copy link

wpsoul commented May 15, 2022

I use subscribe and isSaving. This method is working but has one big problem. Subscribe is working on all user actions. I added console.log to see how many times it’s triggered to check isSaving. And even for simple mouse movement after post is loading, it can call it 200 times and more.

This reduced performance and makes some freezes after some time. So, simple save hook is required like we have in php save_post

@adamsilverstein
Copy link
Member

I started initial work on prototyping a "subscribeOnChange API" based on @youknowriad's suggestion (#17632 (comment)), hopefully this could simplify developer usage when trying to monitor for the completion of any event in Gutenberg where state toggles.

@wpsoul
Copy link

wpsoul commented May 19, 2022

adamsilverstein

subscribe API should not be used for this purpose because this will reduce page perfomance

@adamsilverstein
Copy link
Member

@wpsoul thanks for the feedback. given the project architecture, what alternative would you suggest? What would the ideal API look like for you?

@wpsoul
Copy link

wpsoul commented May 20, 2022

@adamsilverstein

I think it should be javascript/wordpress hook or event which is triggered on post saving procedure.

@adamsilverstein
Copy link
Member

We could add an action hook but it would likely be triggered by the same subscribe method in the PR, so I'm not sure of the advantages. I do it would be easier to use from an action/listener perspective, but part of the resistance to adding this type of hook from the project has been encouraging a shift to more state driven development practices. Therefore, I'm not sure an action hook would be accepted by the project.

@land0r
Copy link

land0r commented Jun 13, 2022

Code above from people works during the whole save process and will be called multiple times. Wanted to share code that will be called only once during the save process:

const [isSavingProcess, setSavingProcess] = useState(false);
  subscribe(() => {
      if (isSavingPost()) {
          setSavingProcess(true);
      } else {
          setSavingProcess(false);
      }
  });
  const updatePost = function () {
      console.log('call once!');
  };

  useEffect(() => {
      if (isSavingProcess) {
          updatePost();
      }
  }, [isSavingProcess]);

Thanks to everyone who shared some code snippets. Still don't understand why Gutenberg has no necessary hooks for such small things which are really needed by the dev community

@wpsoul
Copy link

wpsoul commented Jun 13, 2022

console.log('call once!');

This doesn't save perfomance problem. Because it's still using subscribe and isSavingPost() so will be called hundreds of times when you edit post

@land0r
Copy link

land0r commented Jun 14, 2022

@wpsoul agree, that's just a code snippet as a temporary decision. At least, it is working once as needed for me and maybe for someone else who will need the same functionality

pcraig3 added a commit to cds-snc/gc-articles that referenced this issue Jul 28, 2022
It's not super straightforward how do do this, but there's a really
long github thread you can read about it.

- WordPress/gutenberg#17632 (comment)
pcraig3 added a commit to cds-snc/gc-articles that referenced this issue Jul 29, 2022
* Initial setup

* Setup translationsa

* Refactor to CDS namespace

* Bootstrap the plugin

* Install deps

* Add some basics

* Setup namespace

* Rename globals

* Stub out basic endpoint

* Stub out some endpoints

* add sidebar scaffold

* add to document sidebar

* Return API response for all posts or pages in a given language

* Get started with PHP tests

* Fix up the tests

* Fill out GET route to return the id of translated pages

This route returns post information for a linked post, or a 404 if:
- there is no post for the id given
- there is no translation for the post id given

* Fill out new POST route for assigning a translation to a post/page

There's a helpful WPML action where you can reassign a post's "trid"
value, or set it to `false`, which WPML figures out how to increment it,
but other than that, you're on your own.

This route is deliberately permissive.

There are 4 cases here:

1. New post (no trns) -> other post (no trns)
2. New post (w/ trns) -> other post (no trns)
3. New post (no trns) -> other post (w/ trns)
4. New post (w/ trns) -> other post (w/ trns)

If we prevented scenarios 2, 3, and 4, we would be in a position where you
would have no way to reassign a translation once you assigned one, other than
deleting your post and starting over.

In the scenarios where a translation is already assigned, the logic will
create orphans of the previously-linked posts and make sure the current one is set.

Eg,

Say we have post 1 (EN), post 2 (FR), and post 3 (EN).

If post 1 and 2 are linked, but we submit a request to link 3 -> 2, then
post 1 will become an orphan and 3 -> 2 will be linked.

There are also plenty of ways this can throw back errors, I think I've thought
of all the failure cases.

* Reorganize a bit

* Run plugin setup

* move existing Response tests to Unit folder

* Return pages of any status

* Use WPML get_language_for_element function directly for easier mocking/testing

* checkpoint commit

* Bunch more refactoring

* use sitepress function directly

* Select component pulling in new values properly, but it's messy

Here's the one I'm using:
- https://wordpress.github.io/gutenberg/?path=/docs/components-treeselect--default

* Clean up the component

* Get post type for hint-type messages

* Return flattened array without indexes

* Add a bunch of tests

* Remove old tests

* Run tests in CI

* comments

* Fix path

* Removed Unit test folder

* Fix get translation endpoint

* Refactor helper method to Post class

* add test

* Modify "get translation for post" API route

Instead of returning an API response with the data for the
_translated post_, this endpoint now returns the data for the post
we are referencing in the path, including its associated translated post id.

* Pre-pop the little widget with the current translated ID

* Set hint text in useEffect

* Set new post translation when "Update" button is clicked

It's not super straightforward how do do this, but there's a really
long github thread you can read about it.

- WordPress/gutenberg#17632 (comment)

* Remove separate Gutenberg "plugin", use Dashboard Panel

* Get same post type as the current post

* Add pass rest_url to javascript file via PHP

Previously I had hardcoded it to work for localhost, but one
we release this it needs to know where it's running.

* remove travis file

* remove grunt file

* remove readme

* remove dist file

* update cds mods desc

* cleanup

* cleanup

* cleanup

* move set loading

* add lang files

* Require edit permissions for new translations API

* Change the enqueue

* Return up to 100 posts/pages for translations list

The default number of posts to return is 5, so we weren't seeing them
all show up.

Sources:
- https://developer.wordpress.org/reference/functions/get_posts/#parameters

* Remove errant console.log

* Add user with 'edit_posts' capability in failing tests

* add json

* add source

* Add alias to dockerfile

* Change public instances of 'Wpml' to 'WPML'

I tried to change the classname as well, but I kept getting errors that
were different locally vs on github.

* Fix for language file loading

* Add js build and docker/copy build folders

* Composer install new plugin

* Use correct plugin name in package.json file

Co-authored-by: Dave Samojlenko <[email protected]>
Co-authored-by: Paul Craig <[email protected]>
@rodrigo-arias
Copy link

@csalmeida thanks, your solution worked quite well, although I'm surprised there's still no other way to fire an action after the post-save is done.

@Sidsector9
Copy link
Contributor

Sidsector9 commented Aug 4, 2023

Hello everyone, I am pretty late to this, just wanted to share that I have written a blog post – How to detect when a post is done saving in WordPress Gutenberg? which does it without the direct use of wp.data.subscribe().

import { useBlockProps } from '@wordpress/block-editor';
import { useRef, useState, useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

/**
 * Returns `true` if the post is done saving, `false` otherwise.
 *
 * @returns {Boolean}
 */
const useAfterSave = () => {
    const [ isPostSaved, setIsPostSaved ] = useState( false );
    const isPostSavingInProgress = useRef( false );
    const { isSavingPost, isAutosavingPost } = useSelect( ( __select ) => {
        return {
            isSavingPost: __select( 'core/editor' ).isSavingPost(),
            isAutosavingPost: __select( 'core/editor' ).isAutosavingPost(),
        }
    } );

    useEffect( () => {
        if ( ( isSavingPost || isAutosavingPost ) && ! isPostSavingInProgress.current ) {
            setIsPostSaved( false );
            isPostSavingInProgress.current = true;
        }
        if ( ! ( isSavingPost || isAutosavingPost ) && isPostSavingInProgress.current ) {
            // Code to run after post is done saving.
            setIsPostSaved( true );
            isPostSavingInProgress.current = false;
        }
    }, [ isSavingPost, isAutosavingPost ] );

    return isPostSaved;
};

/**
 * The edit function of an example block.
 *
 * @return {WPElement} Element to render.
 */
export default function Edit() {
    const isAfterSave = useAfterSave();

    useEffect( () => {
        if ( isAfterSave ) {
            // Add your code here that must run after the post is saved.
            console.log( '...done saving...' )
        }
    }, [ isAfterSave ] );

    return (
        <p { ...useBlockProps() }>
            { __( 'Todo List – hello from the editor!', 'todo-list' ) }
        </p>
    );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] Duplicate Used to indicate that a current issue matches an existing one and can be closed
Projects
None yet
Development

No branches or pull requests