Skip to content

Commit

Permalink
Editor: Add subscription action generator for managing sources depend…
Browse files Browse the repository at this point in the history
…encies
  • Loading branch information
aduth committed Jul 5, 2019
1 parent fa46691 commit 062bcd0
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 34 deletions.
113 changes: 100 additions & 13 deletions packages/editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parse,
synchronizeBlocksWithTemplate,
} from '@wordpress/blocks';
import isShallowEqual from '@wordpress/is-shallow-equal';

/**
* Internal dependencies
Expand All @@ -32,8 +33,61 @@ import {
getNotificationArgumentsForSaveFail,
getNotificationArgumentsForTrashFail,
} from './utils/notice-builder';
import { awaitNextStateChange } from './controls';
import * as sources from './block-sources';

/**
* Given a blocks array, returns a blocks array with sourced attribute values
* applied. The reference will remain consistent with the original argument if
* no attribute values must be overridden. If sourced values are applied, the
* return value will be a modified copy of the original array.
*
* @param {WPBlock[]} blocks Original blocks array.
*
* @return {WPBlock[]} Blocks array with sourced values applied.
*/
function* getBlocksWithSourcedAttributes( blocks ) {
let workingBlocks = blocks;
for ( let i = 0; i < blocks.length; i++ ) {
const block = blocks[ i ];
const blockType = yield select( 'core/blocks', 'getBlockType', block.name );

for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) {
if ( ! sources[ schema.source ] || ! sources[ schema.source ].apply ) {
continue;
}

// TODO: This should ideally leverage the `subscribeSources` cache,
// or at worst at least be cached here one-per-source.
const dependencies = yield* sources[ schema.source ].getDependencies();

const sourcedAttributeValue = sources[ schema.source ].apply( schema, dependencies );

// It's only necessary to apply the value if it differs from the
// block's locally-assigned value, to avoid needlessly resetting
// the block editor.
if ( sourcedAttributeValue === block.attributes[ attributeName ] ) {
continue;
}

// Create a shallow clone to mutate, leaving the original intact.
if ( workingBlocks === blocks ) {
workingBlocks = [ ...workingBlocks ];
}

workingBlocks.splice( i, 1, {
...block,
attributes: {
...block.attributes,
[ attributeName ]: sourcedAttributeValue,
},
} );
}
}

return workingBlocks;
}

/**
* Returns an action generator used in signalling that editor has initialized with
* the specified post object and editor settings.
Expand Down Expand Up @@ -70,6 +124,7 @@ export function* setupEditor( post, edits, template ) {

yield setupEditorState( post );
yield resetEditorBlocks( blocks );
yield* subscribeSources();
}

/**
Expand All @@ -82,6 +137,50 @@ export function tearDownEditor() {
return { type: 'TEAR_DOWN_EDITOR' };
}

/**
* Returns an action generator which loops to await the next state change,
* calling to reset blocks when a block source dependencies change.
*
* @yield {Object} Action object.
*/
export const subscribeSources = ( () => {
const lastDependencies = new WeakMap();

return function* () {
while ( true ) {
yield awaitNextStateChange();

// The bailout case: If the editor becomes unmounted, it will flag
// itself as non-ready. Effectively unsubscribes from the registry.
const isStillReady = yield select( 'core/editor', '__unstableIsEditorReady' );
if ( ! isStillReady ) {
break;
}

let reset = false;
for ( const source of Object.values( sources ) ) {
if ( ! source.getDependencies ) {
continue;
}

const dependencies = yield* source.getDependencies();
if ( ! isShallowEqual( dependencies, lastDependencies.get( source ) ) ) {
// Allow the loop to continue in order to assign latest
// dependencies values, but mark for reset. Avoid reset
// from the first assignment.
reset = reset || lastDependencies.has( source );

lastDependencies.set( source, dependencies );
}
}

if ( reset ) {
yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ) );
}
}
};
} )();

/**
* Action generator function used in signalling that sources are to be updated
* in response to a single block attributes update.
Expand Down Expand Up @@ -762,21 +861,9 @@ export function unlockPostSaving( lockName ) {
* @return {Object} Action object
*/
export function* resetEditorBlocks( blocks, options = {} ) {
for ( const source in Object.values( sources ) ) {
if ( typeof source.applyAll === 'function' ) {
blocks = yield* source.applyAll( blocks );
}

if ( typeof source.apply === 'function' ) {
for ( let i = 0; i < blocks.length; i++ ) {
blocks[ i ] = yield* source.apply( blocks[ i ] );
}
}
}

return {
type: 'RESET_EDITOR_BLOCKS',
blocks,
blocks: yield* getBlocksWithSourcedAttributes( blocks ),
shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false,
};
}
Expand Down
41 changes: 22 additions & 19 deletions packages/editor/src/store/block-sources/meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,31 @@ import { select } from '@wordpress/data-controls';
import { editPost } from '../actions';

/**
* Store control which, given an array of blocks, modifies block entries which
* source from meta properties to assign the attribute values.
* Store control invoked upon a state changes, responsible for returning an
* object of dependencies. When a change in dependencies occurs (by shallow
* equality of the returned object), blocks are reset to apply the new sourced
* value.
*
* @param {WPBlock[]} blocks Blocks array.
*
* @yield {Object} Yielded action objects or store controls.
* @return {WPBlock[]} Modified blocks array.
* @yield {Object} Optional yielded controls.
* @return {Object} Dependencies as object.
*/
export function* applyAll( blocks ) {
const meta = yield select( 'core/editor', 'getEditedPostAttribute', 'meta' );

for ( let i = 0; i < blocks.length; i++ ) {
const block = blocks[ i ];
const blockType = yield select( 'core/blocks', 'getBlockType', block.name );
for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) {
if ( schema.source === 'meta' ) {
blocks[ i ].attributes[ attributeName ] = meta[ schema.meta ];
}
}
}
export function* getDependencies() {
return {
meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ),
};
}

return blocks;
/**
* Given an attribute schema and dependencies data, returns a source value.
*
* @param {Object} schema Block type attribute schema.
* @param {Object} dependencies Source dependencies.
* @param {Object} dependencies.meta Post meta.
*
* @return {Object} Block attribute value.
*/
export function apply( schema, { meta } ) {
return meta[ schema.meta ];
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/editor/src/store/controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* WordPress dependencies
*/
import { createRegistryControl } from '@wordpress/data';

/**
* Returns an control descriptor signalling to subscribe to the registry and
* resolve the control promise only when the next state change occurs.
*
* @return {Object} Control descriptor.
*/
export function awaitNextStateChange() {
return { type: 'AWAIT_NEXT_STATE_CHANGE' };
}

const controls = {
AWAIT_NEXT_STATE_CHANGE: createRegistryControl(
( registry ) => () => new Promise( ( resolve ) => {
const unsubscribe = registry.subscribe( () => {
unsubscribe();
resolve();
} );
} )
),
};

export default controls;
8 changes: 6 additions & 2 deletions packages/editor/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { controls as dataControls } from '@wordpress/data-controls';

/**
* Internal dependencies
Expand All @@ -11,6 +11,7 @@ import reducer from './reducer';
import applyMiddlewares from './middlewares';
import * as selectors from './selectors';
import * as actions from './actions';
import controls from './controls';
import { STORE_KEY } from './constants';

/**
Expand All @@ -24,7 +25,10 @@ export const storeConfig = {
reducer,
selectors,
actions,
controls,
controls: {
...dataControls,
...controls,
},
};

const store = registerStore( STORE_KEY, {
Expand Down

0 comments on commit 062bcd0

Please sign in to comment.