Skip to content

Commit

Permalink
Block Editor: Reimplement persistent block change as reducer state
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Feb 19, 2019
1 parent d993b49 commit 208991f
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 254 deletions.
166 changes: 13 additions & 153 deletions packages/block-editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,126 +3,14 @@
*/
import { Component } from '@wordpress/element';
import { DropZoneProvider, SlotFillProvider } from '@wordpress/components';
import { withDispatch, withSelect, RegistryConsumer } from '@wordpress/data';
import { compose, createHigherOrderComponent } from '@wordpress/compose';
import isShallowEqual from '@wordpress/is-shallow-equal';

/**
* Higher-order component which renders the original component with the current
* registry context passed as its `registry` prop.
*
* @param {WPComponent} OriginalComponent Original component.
*
* @return {WPComponent} Enhanced component.
*/
const withRegistry = createHigherOrderComponent(
( OriginalComponent ) => ( props ) => (
<RegistryConsumer>
{ ( registry ) => (
<OriginalComponent
{ ...props }
registry={ registry }
/>
) }
</RegistryConsumer>
),
'withRegistry'
);

/**
* Returns true if the two object arguments have the same keys, or false
* otherwise.
*
* @param {Object} a First object.
* @param {Object} b Second object.
*
* @return {boolean} Whether the two objects have the same keys.
*/
export function hasSameKeys( a, b ) {
return !! a && !! b && isShallowEqual( Object.keys( a ), Object.keys( b ) );
}

/**
* Returns true if, given the currently dispatching action and the previously
* dispatched action, the two actions are updating the same block attribute, or
* false otherwise.
*
* @param {Object} action Currently dispatching action.
* @param {Object} lastAction Previously dispatched action.
*
* @return {boolean} Whether actions are updating the same block attribute.
*/
export function isUpdatingSameBlockAttribute( action, lastAction ) {
return (
action.type === 'UPDATE_BLOCK_ATTRIBUTES' &&
action.clientId === lastAction.clientId &&
hasSameKeys( action.attributes, lastAction.attributes )
);
}

/**
* Given a data namespace store and a callback, returns a substitute dispatch
* function which preserves the original dispatch behavior and invokes the
* callback when a blocks state change should be committed.
*
* @param {WPDataNamespaceStore} store Store for which to create replacement
* dispatch function.
* @param {Function} callback Function to call when blocks state
* should be committed.
*
* @return {Function} Enhanced store dispatch function.
*/
function createChangeObserver( store, callback ) {
let lastAction, lastState, isPendingCommit;

function dispatch( action ) {
const result = dispatch._originalDispatch( action );
const state = store.getState();

if ( action.type === 'RESET_BLOCKS' ) {
// Consider block reset as superseding any pending change commits,
// even if destructive to pending user commits. It should rarely be
// the case that blocks are suddenly reset while user interacts.
isPendingCommit = false;
} else if ( lastAction && lastState && state !== lastState ) {
if (
state.blocks !== lastState.blocks &&
isUpdatingSameBlockAttribute( action, lastAction )
) {
// So long as block updates occur as operating on the same
// attributes in the previous action, delay callback.
isPendingCommit = true;
} else if ( isPendingCommit ) {
// Once any other action occurs while pending commit, release
// the deferred callback as completed.
callback();
isPendingCommit = false;
}
}

lastAction = action;
lastState = state;

return result;
}

dispatch._originalDispatch = store.dispatch;

return dispatch;
}
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';

class BlockEditorProvider extends Component {
constructor() {
super( ...arguments );

this.onChange = this.onChange.bind( this );
}

componentDidMount() {
this.isSyncingBlockValue = true;
this.props.updateEditorSettings( this.props.settings );
this.props.resetBlocks( this.props.value );
this.attachChangeObserver( this.props.registry );
}

componentDidUpdate( prevProps ) {
Expand All @@ -133,60 +21,29 @@ class BlockEditorProvider extends Component {
resetBlocks,
blocks,
onInput,
registry,
onChange,
isLastBlockChangePersistent,
} = this.props;

if ( settings !== prevProps.settings ) {
updateEditorSettings( settings );
}

if ( registry !== prevProps.registry ) {
this.attachChangeObserver( registry, prevProps.registry );
}

if ( this.isSyncingBlockValue ) {
this.isSyncingBlockValue = false;
} else if ( blocks !== prevProps.blocks ) {
this.isSyncingBlockValue = true;
onInput( blocks );

if ( isLastBlockChangePersistent ) {
onChange( blocks );
}
} else if ( value !== prevProps.value ) {
this.isSyncingBlockValue = true;
resetBlocks( value );
}
}

/**
* Calls the mounted instance's `onChange` prop callback with the current
* blocks prop value.
*/
onChange() {
this.props.onChange( this.props.blocks );
}

/**
* Given a registry object, overrides the default dispatch behavior for the
* `core/block-editor` store to interpret a state change which should be
* considered as calling the mounted instance's `onChange` callback. Unlike
* `onInput` which is called for any change in block state, `onChange` is
* only called for meaningful commit interactions. If a second registry
* argument is passed, it is treated as the previous registry to which the
* dispatch behavior was overridden, and the original dispatch is restored.
*
* @param {WPDataRegistry} registry Registry from which block editor
* dispatch is to be overriden.
* @param {WPDataRegistry} prevRegistry Previous registry whose dispatch
* behavior should be restored.
*/
attachChangeObserver( registry, prevRegistry ) {
const { store } = registry.namespaces[ 'core/block-editor' ];
store.dispatch = createChangeObserver( store, this.onChange );

if ( prevRegistry ) {
const { store: prevStore } = prevRegistry.namespaces[ 'core/block-editor' ];
prevStore.dispatch = prevStore.dispatch._originalDispatch;
}
}

render() {
const { children } = this.props;

Expand All @@ -202,10 +59,14 @@ class BlockEditorProvider extends Component {

export default compose( [
withSelect( ( select ) => {
const { getBlocks } = select( 'core/block-editor' );
const {
getBlocks,
isLastBlockChangePersistent,
} = select( 'core/block-editor' );

return {
blocks: getBlocks(),
isLastBlockChangePersistent: isLastBlockChangePersistent(),
};
} ),
withDispatch( ( dispatch ) => {
Expand All @@ -219,5 +80,4 @@ export default compose( [
resetBlocks,
};
} ),
withRegistry,
] )( BlockEditorProvider );
95 changes: 0 additions & 95 deletions packages/block-editor/src/components/provider/test/index.js

This file was deleted.

64 changes: 61 additions & 3 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,65 @@ function getMutateSafeObject( original, working ) {
}

/**
* Higher-order reducer targeting the combined editor reducer, augmenting
* Returns true if the two object arguments have the same keys, or false
* otherwise.
*
* @param {Object} a First object.
* @param {Object} b Second object.
*
* @return {boolean} Whether the two objects have the same keys.
*/
export function hasSameKeys( a, b ) {
return isEqual( Object.keys( a ), Object.keys( b ) );
}

/**
* Returns true if, given the currently dispatching action and the previously
* dispatched action, the two actions are updating the same block attribute, or
* false otherwise.
*
* @param {Object} action Currently dispatching action.
* @param {Object} lastAction Previously dispatched action.
*
* @return {boolean} Whether actions are updating the same block attribute.
*/
export function isUpdatingSameBlockAttribute( action, lastAction ) {
return (
action.type === 'UPDATE_BLOCK_ATTRIBUTES' &&
action.clientId === lastAction.clientId &&
hasSameKeys( action.attributes, lastAction.attributes )
);
}

/**
* Higher-order reducer intended to augment the blocks reducer, assigning an
* `isPersistentChange` property value corresponding to whether a change in
* state can be considered as persistent. All changes are considered persistent
* except when updating the same block attribute as in the previous action.
*
* @param {Function} reducer Original reducer function.
*
* @return {Function} Enhanced reducer function.
*/
function withPersistentBlockChange( reducer ) {
let lastAction;

return ( state, action ) => {
const nextState = reducer( state, action );
if ( state !== nextState ) {
nextState.isPersistentChange = (
! isUpdatingSameBlockAttribute( action, lastAction )
);
}

lastAction = action;

return nextState;
};
}

/**
* Higher-order reducer targeting the combined blocks reducer, augmenting
* block client IDs in remove action to include cascade of inner blocks.
*
* @param {Function} reducer Original reducer function.
Expand Down Expand Up @@ -235,8 +293,7 @@ const withSaveReusableBlock = ( reducer ) => ( state, action ) => {
};

/**
* Reducer returning the editor post state, including blocks parsed from
* current HTML markup.
* Reducer returning the blocks state.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
Expand All @@ -248,6 +305,7 @@ export const blocks = flow(
withInnerBlocksRemoveCascade,
withBlockReset,
withSaveReusableBlock,
withPersistentBlockChange,
)( {
byClientId( state = {}, action ) {
switch ( action.type ) {
Expand Down
Loading

0 comments on commit 208991f

Please sign in to comment.