Skip to content

Commit

Permalink
Metaboxes: Prefetch Metaboxes and don't reload them on save
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Nov 28, 2017
1 parent c575dd5 commit 10acc3e
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 437 deletions.
33 changes: 15 additions & 18 deletions docs/meta-box.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# Meta Boxes

This is a brief document detailing how meta box support works in Gutenberg. With
the superior developer and user experience of blocks however, especially once,
block templates are available, **converting PHP meta boxes to blocks is highly
encouraged!**
This is a brief document detailing how meta box support works in Gutenberg. With the superior developer and user experience of blocks however, especially once, block templates are available, **converting PHP meta boxes to blocks is highly encouraged!**

### Testing, Converting, and Maintaining Existing Meta Boxes

Expand Down Expand Up @@ -39,12 +36,10 @@ When Gutenberg is run, this meta box will no longer be displayed in the meta box

### Meta Box Data Collection

On each Gutenberg page load, the global state of post.php is mimicked, this is hooked in as far back as `plugins_loaded`.
On each Gutenberg page load, we register an action that collects the meta box data to determine if an area is empty. The original global state is reset upon collection of meta box data.

See `lib/register.php gutenberg_trick_plugins_into_registering_meta_boxes()`

This will register an action that collects the meta box data to determine if an area is empty. The original global state is reset upon collection of meta box data.

`gutenberg_collect_meta_box_data()` is hooked in later on `admin_head`. It will run through the functions and hooks that `post.php` runs to register meta boxes; namely `add_meta_boxes`, `add_meta_boxes_{$post->post_type}`, and `do_meta_boxes`.

A copy of the global `$wp_meta_boxes` is made then filtered through `apply_filters( 'filter_gutenberg_meta_boxes', $_meta_boxes_copy );`, which will strip out any core meta boxes, standard custom taxonomy meta boxes, and any meta boxes that have declared themselves as only existing for backwards compatibility purposes.
Expand All @@ -55,27 +50,29 @@ Ideally, this could be done at instantiation of the editor and help simplify thi

### Redux and React Meta Box Management

*The Redux store by default will hold all meta boxes as inactive*. When `INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box areas by setting the `isActive` flag to `true`. Once this happens React will check for the new props sent in by Redux on the `MetaBox` component. If that `MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will be rendered. The `MetaBox` component is the container component that mediates between the `MetaBoxArea` and the Redux Store. *If no meta boxes are active, nothing happens. This will be the default behavior, as all core meta boxes have been stripped.*
When rendering the Gutenberg Page, the metaboxes are rendered to a hidden div `#metaboxes`.

*The Redux store by default will hold all meta boxes as inactive*. When
`INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box areas by setting the `isActive` flag to `true`. Once this happens React will check for the new props sent in by Redux on the `MetaBox` component. If that `MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will be rendered. The `MetaBox` component is the container component that mediates between the `MetaBoxArea` and the Redux Store. *If no meta boxes are active, nothing happens. This will be the default behavior, as all core meta boxes have been stripped.*

#### MetaBoxArea Component

When the component renders it will store a ref to the metaboxes container, calls the page rendering the metaboxes and watches input and changes.
The change detection will store the current form's `FormData`, then whenever a change is detected the current form data will be checked vs, the original form data. This serves as a way to see if the meta box state is dirty. When the meta box state has been detected to have changed, a Redux action `META_BOX_STATE_CHANGED` is dispatched, updating the store setting the isDirty flag to `true`. If the state ever returns back to the original form data, `META_BOX_STATE_CHANGED` is dispatched again to set the isDirty flag to `false`. A selector `isMetaBoxStateDirty()` is used to help check whether the post can be updated. It checks each meta box for whether it is dirty, and if there is at least one dirty meta box, it will return true. This dirty detection does not impact creating new posts, as the content will have to change before meta boxes can trigger the overall dirty state.
When the component renders it will store a ref to the metaboxes container, retrieve the metaboxes HTML from the prefetch location and watches input and changes.

When the post is updated, only meta boxes that are active and dirty, will be submitted. This removes any unnecessary requests being made. No extra revisions, are created either by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any dirty meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`, the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission.
The change detection will store the current form's `FormData`, then whenever a change is detected the current form data will be checked vs, the original form data. This serves as a way to see if the meta box state is dirty. When the meta box state has been detected to have changed, a Redux action `META_BOX_STATE_CHANGED` is dispatched, updating the store setting the isDirty flag to `true`. If the state ever returns back to the original form data, `META_BOX_STATE_CHANGED` is dispatched again to set the isDirty flag to `false`. A selector `isMetaBoxStateDirty()` is used to help check whether the post can be updated. It checks each meta box for whether it is dirty, and if there is at least one dirty meta box, it will return true. This dirty detection does not impact creating new posts, as the content will have to change before meta boxes can trigger the overall dirty state.

Since the meta box updating is being triggered on post save success, we check to see if the post is saving and display an updating overlay, to prevent users from changing the form values while the meta box is submitting. The saving overlay could be made transparent, to give a more seamless effect.
When the post is updated, only meta boxes areas that are active and dirty, will be submitted. This removes any unnecessary requests being made. No extra revisions, are created either by the meta box submissions. A Redux action will trigger on `REQUEST_POST_UPDATE` for any dirty meta box. See `editor/effects.js`. The `REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`, the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form submission.

Each `MetaBoxArea` will point to an individual source. These are partial pages being served by post.php. Why this approach? By using post.php directly, we don't have to worry as much about getting the global state 100% correct for each and every use case of a meta box, especially when it comes to saving. Essentially, when post.php loads it will set up all of its state correctly, and when it hits the three `do_action( 'do_meta_boxes' )` hooks it will trigger our partial page.
If the metabox area is saving, we display an updating overlay, to prevent users from changing the form values while the meta box is submitting.

When the new block editor was made into the default editor it is now required to provide the classic-editor flag to access the metabox partial page.

`gutenberg_meta_box_partial_page()` is used to render the meta boxes for a context then exit the execution thread early. A `meta_box` request parameter is used to trigger this early exit. The `meta_box` request parameter should match one of `'advanced'`, `'normal'`, or `'side'`. This value will determine which meta box area is served. So an example url would look like:
`gutenberg_meta_box_save()` is used to save the meta boxes changes. A `meta_box` request parameter should be present and should match one of `'advanced'`, `'normal'`, or `'side'`. This value will determine which meta box area is served.

`mysite.com/wp-admin/post.php?post=1&action=edit&meta_box=$location&classic-editor`
So an example url would look like:

This url is automatically passed into React via a `_wpMetaBoxUrl` global variable. The partial page is very similar to post.php and pretty much imitates it and after rendering the meta boxes via `do_meta_boxes()` it imitates `admin_footer`, exits early, and does some hook clean up.
`mysite.com/wp-admin/post.php?post=1&action=edit&meta_box=$location&classic-editor`

These styles make use of some of the SASS variables, so that as the Gutenberg UI updates so will the meta boxes.
This url is automatically passed into React via a `_wpMetaBoxUrl` global variable.

The partial page mimics the `post.php` post form, so when it is submitted it will normally fire all of the necessary hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission the page will be reloaded back to the same partial page with updated data. React will signal a `handleMetaBoxReload` to set up the new form state for dirty checking, remove the updating overlay, and set the store to no longer be updating the meta box area.
Thus page page mimics the `post.php` post form, so when it is submitted it will normally fire all of the necessary hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to set up the new form state for dirty checking, remove the updating overlay, and set the store to no longer be updating the meta box area.
48 changes: 20 additions & 28 deletions editor/components/meta-boxes/meta-boxes-area/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class MetaBoxesArea extends Component {
super( ...arguments );

this.state = {
loading: true,
loading: false,
};
this.originalFormData = [];
this.bindNode = this.bindNode.bind( this );
Expand All @@ -40,9 +40,10 @@ class MetaBoxesArea extends Component {
this.fetchMetaboxes();
}

componentWillUnmout() {
componentWillUnmount() {
this.mounted = false;
this.unbindFormEvents();
document.querySelector( '#metaboxes' ).appendChild( this.form );
}

unbindFormEvents() {
Expand All @@ -63,37 +64,28 @@ class MetaBoxesArea extends Component {
body: new window.FormData( this.form ),
credentials: 'include',
};
const request = window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), fetchOptions );
this.onMetaboxResponse( request, false );
this.unbindFormEvents();

// Save the metaboxes
window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), fetchOptions )
.then( () => {
if ( ! this.mounted ) {
return false;
}
this.setState( { loading: false } );
this.props.metaBoxReloaded( location );
} );
}
}

fetchMetaboxes() {
const { location } = this.props;
const request = window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), { credentials: 'include' } );
this.onMetaboxResponse( request );
}

onMetaboxResponse( request, initial = true ) {
request.then( ( response ) => response.text() )
.then( ( body ) => {
if ( ! this.mounted ) {
return;
}
jQuery( this.node ).html( body );
this.form = this.node.querySelector( '.meta-box-form' );
this.form.onSubmit = ( event ) => event.preventDefault();
this.originalFormData = this.getFormData();
this.form.addEventListener( 'change', this.checkState );
this.form.addEventListener( 'input', this.checkState );
this.setState( { loading: false } );
if ( ! initial ) {
this.props.metaBoxReloaded( this.props.location );
} else {
this.props.metaBoxLoaded( this.props.location );
}
} );
this.form = document.querySelector( '.metabox-location-' + location );
this.node.appendChild( this.form );
this.form.onSubmit = ( event ) => event.preventDefault();
this.originalFormData = this.getFormData();
this.form.addEventListener( 'change', this.checkState );
this.form.addEventListener( 'input', this.checkState );
this.props.metaBoxLoaded( location );
}

getFormData() {
Expand Down
24 changes: 1 addition & 23 deletions editor/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
import { get, uniqueId, map, filter, some, castArray } from 'lodash';
import { get, uniqueId, castArray, map } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -46,7 +46,6 @@ import {
isEditedPostDirty,
isEditedPostNew,
isEditedPostSaveable,
getMetaBoxes,
getBlock,
getReusableBlock,
} from './selectors';
Expand Down Expand Up @@ -299,27 +298,6 @@ export default {

return effects;
},
INITIALIZE_META_BOX_STATE( action ) {
// Hold jquery.ready until the metaboxes load
const locations = [ 'normal', 'side' ];
if ( some( locations, ( location ) => !! action.metaBoxes[ location ] ) ) {
jQuery.holdReady( true );
}
},
META_BOX_LOADED( action, store ) {
const { getState } = store;
const metaboxes = getMetaBoxes( getState() );
const unloadedMetaboxes = filter(
map( metaboxes, ( value, key ) => ( {
...value,
key,
} ) ),
( metabox ) => metabox.isActive && ! metabox.isLoaded
);
if ( unloadedMetaboxes.length === 1 && unloadedMetaboxes[ 0 ].key === action.location ) {
jQuery.holdReady( false );
}
},
FETCH_REUSABLE_BLOCKS( action, store ) {
const { id } = action;
const { dispatch } = store;
Expand Down
3 changes: 3 additions & 0 deletions gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ function the_gutenberg_project() {
<div class="nvda-temp-fix screen-reader-text">&nbsp;</div>
<div class="gutenberg">
<div id="editor" class="gutenberg__editor"></div>
<div id="metaboxes" style="display: none;">
<?php the_gutenberg_metaboxes(); ?>
</div>
</div>
<?php
}
Expand Down
2 changes: 1 addition & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
wp_enqueue_script(
'wp-editor',
gutenberg_url( 'editor/build/index.js' ),
array( 'jquery', 'wp-api', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils', 'word-count', 'editor', 'heartbeat' ),
array( 'wp-api', 'wp-date', 'wp-i18n', 'wp-blocks', 'wp-element', 'wp-components', 'wp-utils', 'word-count', 'editor', 'heartbeat' ),
filemtime( gutenberg_dir_path() . 'editor/build/index.js' ),
true // enqueue in the footer.
);
Expand Down
Loading

0 comments on commit 10acc3e

Please sign in to comment.