diff --git a/package-lock.json b/package-lock.json
index 3bc1f7d1b27788..6195daff5a18df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4885,6 +4885,7 @@
"@wordpress/api-fetch": "file:packages/api-fetch",
"@wordpress/data": "file:packages/data",
"@wordpress/deprecated": "file:packages/deprecated",
+ "@wordpress/element": "file:packages/element",
"@wordpress/is-shallow-equal": "file:packages/is-shallow-equal",
"@wordpress/url": "file:packages/url",
"equivalent-key-map": "^0.2.2",
diff --git a/packages/core-data/package.json b/packages/core-data/package.json
index 3da791a43f81c7..7b46a527f675ca 100644
--- a/packages/core-data/package.json
+++ b/packages/core-data/package.json
@@ -26,6 +26,7 @@
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/data": "file:../data",
"@wordpress/deprecated": "file:../deprecated",
+ "@wordpress/element": "file:../element",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/url": "file:../url",
"equivalent-key-map": "^0.2.2",
diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js
new file mode 100644
index 00000000000000..7870e0217cc974
--- /dev/null
+++ b/packages/core-data/src/entity-provider.js
@@ -0,0 +1,90 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext, useContext, useCallback } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { defaultEntities, kinds } from './entities';
+
+const entities = {
+ ...defaultEntities.reduce( ( acc, entity ) => {
+ if ( ! acc[ entity.kind ] ) {
+ acc[ entity.kind ] = {};
+ }
+ acc[ entity.kind ][ entity.name ] = { context: createContext() };
+ return acc;
+ }, {} ),
+ ...kinds.reduce( ( acc, kind ) => {
+ acc[ kind.name ] = {};
+ return acc;
+ }, {} ),
+};
+const getEntity = ( kind, type ) => {
+ if ( ! entities[ kind ] ) {
+ throw new Error( `Missing entity config for kind: ${ kind }.` );
+ }
+
+ if ( ! entities[ kind ][ type ] ) {
+ entities[ kind ][ type ] = { context: createContext() };
+ }
+
+ return entities[ kind ][ type ];
+};
+
+/**
+ * Context provider component for providing
+ * an entity for a specific entity type.
+ *
+ * @param {Object} props The component's props.
+ * @param {string} props.kind The entity kind.
+ * @param {string} props.type The entity type.
+ * @param {number} props.id The entity ID.
+ * @param {*} props.children The children to wrap.
+ *
+ * @return {Object} The provided children, wrapped with
+ * the entity's context provider.
+ */
+export default function EntityProvider( { kind, type, id, children } ) {
+ const Provider = getEntity( kind, type ).context.Provider;
+ return { children };
+}
+
+/**
+ * Hook that returns the value and a setter for the
+ * specified property of the nearest provided
+ * entity of the specified type.
+ *
+ * @param {string} kind The entity kind.
+ * @param {string} type The entity type.
+ * @param {string} prop The property name.
+ *
+ * @return {[*, Function]} A tuple where the first item is the
+ * property value and the second is the
+ * setter.
+ */
+export function useEntityProp( kind, type, prop ) {
+ const id = useContext( getEntity( kind, type ).context );
+
+ const value = useSelect(
+ ( select ) => {
+ const entity = select( 'core' ).getEditedEntityRecord( kind, type, id );
+ return entity && entity[ prop ];
+ },
+ [ kind, type, id, prop ]
+ );
+
+ const { editEntityRecord } = useDispatch( 'core' );
+ const setValue = useCallback(
+ ( newValue ) => {
+ editEntityRecord( kind, type, id, {
+ [ prop ]: newValue,
+ } );
+ },
+ [ kind, type, id, prop ]
+ );
+
+ return [ value, setValue ];
+}
diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js
index b0c5718ab9b888..2cdddb960e448b 100644
--- a/packages/core-data/src/index.js
+++ b/packages/core-data/src/index.js
@@ -48,3 +48,5 @@ registerStore( REDUCER_KEY, {
selectors: { ...selectors, ...entitySelectors },
resolvers: { ...resolvers, ...entityResolvers },
} );
+
+export { default as EntityProvider, useEntityProp } from './entity-provider';
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 6cd07c572211af..c386f02097167b 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -11,6 +11,7 @@ import { compose } from '@wordpress/compose';
import { Component } from '@wordpress/element';
import { withDispatch, withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
+import { EntityProvider } from '@wordpress/core-data';
import { BlockEditorProvider, transformStyles } from '@wordpress/block-editor';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
@@ -164,6 +165,7 @@ class EditorProvider extends Component {
const {
canUserUseUnfilteredHTML,
children,
+ post,
blocks,
resetEditorBlocks,
isReady,
@@ -185,18 +187,20 @@ class EditorProvider extends Component {
);
return (
-
- { children }
-
-
- { editorSettings.__experimentalBlockDirectory && }
-
+
+
+ { children }
+
+
+ { editorSettings.__experimentalBlockDirectory && }
+
+
);
}
}
diff --git a/packages/editor/src/hooks/custom-sources-backwards-compatibility.js b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js
new file mode 100644
index 00000000000000..0e6173b99382d6
--- /dev/null
+++ b/packages/editor/src/hooks/custom-sources-backwards-compatibility.js
@@ -0,0 +1,73 @@
+/**
+ * WordPress dependencies
+ */
+import { getBlockType } from '@wordpress/blocks';
+import { useEntityProp } from '@wordpress/core-data';
+import { useMemo, useCallback } from '@wordpress/element';
+import { createHigherOrderComponent } from '@wordpress/compose';
+import { addFilter } from '@wordpress/hooks';
+
+const EMPTY_OBJECT = {};
+function useMetaAttributeSource( name, _attributes, _setAttributes ) {
+ const { attributes: attributeTypes = EMPTY_OBJECT } =
+ getBlockType( name ) || EMPTY_OBJECT;
+ let [ attributes, setAttributes ] = [ _attributes, _setAttributes ];
+
+ if ( Object.values( attributeTypes ).some( ( type ) => type.source === 'meta' ) ) {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [ meta, setMeta ] = useEntityProp( 'postType', 'post', 'meta' );
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ attributes = useMemo(
+ () => ( {
+ ..._attributes,
+ ...Object.keys( attributeTypes ).reduce( ( acc, key ) => {
+ if ( attributeTypes[ key ].source === 'meta' ) {
+ acc[ key ] = meta[ attributeTypes[ key ].meta ];
+ }
+ return acc;
+ }, {} ),
+ } ),
+ [ attributeTypes, meta, _attributes ]
+ );
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ setAttributes = useCallback(
+ ( ...args ) => {
+ Object.keys( args[ 0 ] ).forEach( ( key ) => {
+ if ( attributeTypes[ key ].source === 'meta' ) {
+ setMeta( { [ attributeTypes[ key ].meta ]: args[ 0 ][ key ] } );
+ }
+ } );
+ return _setAttributes( ...args );
+ },
+ [ attributeTypes, setMeta, _setAttributes ]
+ );
+ }
+
+ return [ attributes, setAttributes ];
+}
+const withMetaAttributeSource = createHigherOrderComponent(
+ ( BlockListBlock ) => ( { attributes, setAttributes, name, ...props } ) => {
+ [ attributes, setAttributes ] = useMetaAttributeSource(
+ name,
+ attributes,
+ setAttributes
+ );
+ return (
+
+ );
+ },
+ 'withMetaAttributeSource'
+);
+
+addFilter(
+ 'editor.BlockListBlock',
+ 'core/editor/custom-sources-backwards-compatibility/with-meta-attribute-source',
+ withMetaAttributeSource
+);
diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js
index 2c8a61d9802521..6e0934d63c0cfa 100644
--- a/packages/editor/src/hooks/index.js
+++ b/packages/editor/src/hooks/index.js
@@ -1,4 +1,5 @@
/**
* Internal dependencies
*/
+import './custom-sources-backwards-compatibility';
import './default-autocompleters';
diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js
index bd3076ef7c3b57..a95fcce44db1ed 100644
--- a/packages/editor/src/store/actions.js
+++ b/packages/editor/src/store/actions.js
@@ -9,7 +9,6 @@ import { has, castArray } from 'lodash';
import deprecated from '@wordpress/deprecated';
import { dispatch, select, apiFetch } from '@wordpress/data-controls';
import { parse, synchronizeBlocksWithTemplate } from '@wordpress/blocks';
-import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
@@ -25,120 +24,6 @@ import {
getNotificationArgumentsForTrashFail,
} from './utils/notice-builder';
import serializeBlocks from './utils/serialize-blocks';
-import { awaitNextStateChange, getRegistry } from './controls';
-import * as sources from './block-sources';
-
-/**
- * Map of Registry instance to WeakMap of dependencies by custom source.
- *
- * @type WeakMap>
- */
-const lastBlockSourceDependenciesByRegistry = new WeakMap;
-
-/**
- * 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 ) {
- const registry = yield getRegistry();
- if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) {
- return blocks;
- }
-
- const blockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry );
-
- let workingBlocks = blocks;
- for ( let i = 0; i < blocks.length; i++ ) {
- let 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;
- }
-
- if ( ! blockSourceDependencies.has( sources[ schema.source ] ) ) {
- continue;
- }
-
- const dependencies = blockSourceDependencies.get( sources[ schema.source ] );
- 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 ];
- }
-
- block = {
- ...block,
- attributes: {
- ...block.attributes,
- [ attributeName ]: sourcedAttributeValue,
- },
- };
-
- workingBlocks.splice( i, 1, block );
- }
-
- // Recurse to apply source attributes to inner blocks.
- if ( block.innerBlocks.length ) {
- const appliedInnerBlocks = yield* getBlocksWithSourcedAttributes( block.innerBlocks );
- if ( appliedInnerBlocks !== block.innerBlocks ) {
- if ( workingBlocks === blocks ) {
- workingBlocks = [ ...workingBlocks ];
- }
-
- block = {
- ...block,
- innerBlocks: appliedInnerBlocks,
- };
-
- workingBlocks.splice( i, 1, block );
- }
- }
- }
-
- return workingBlocks;
-}
-
-/**
- * Refreshes the last block source dependencies, optionally for a given subset
- * of sources (defaults to the full set of sources).
- *
- * @param {?Array} sourcesToUpdate Optional subset of sources to reset.
- *
- * @yield {Object} Yielded actions or control descriptors.
- */
-function* resetLastBlockSourceDependencies( sourcesToUpdate = Object.values( sources ) ) {
- if ( ! sourcesToUpdate.length ) {
- return;
- }
-
- const registry = yield getRegistry();
- if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) {
- lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap );
- }
-
- const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry );
-
- for ( const source of sourcesToUpdate ) {
- const dependencies = yield* source.getDependencies();
- lastBlockSourceDependencies.set( source, dependencies );
- }
-}
/**
* Returns an action generator used in signalling that editor has initialized with
@@ -168,7 +53,6 @@ export function* setupEditor( post, edits, template ) {
}
yield resetPost( post );
- yield* resetLastBlockSourceDependencies();
yield {
type: 'SETUP_EDITOR',
post,
@@ -186,7 +70,6 @@ export function* setupEditor( post, edits, template ) {
) {
yield editPost( edits );
}
- yield* __experimentalSubscribeSources();
}
/**
@@ -199,55 +82,6 @@ export function __experimentalTearDownEditor() {
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 function* __experimentalSubscribeSources() {
- 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( STORE_KEY, '__unstableIsEditorReady' );
- if ( ! isStillReady ) {
- break;
- }
-
- const registry = yield getRegistry();
-
- let reset = false;
- for ( const source of Object.values( sources ) ) {
- if ( ! source.getDependencies ) {
- continue;
- }
-
- const dependencies = yield* source.getDependencies();
-
- if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) {
- lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap );
- }
-
- const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry );
- const lastDependencies = lastBlockSourceDependencies.get( source );
-
- if ( ! isShallowEqual( dependencies, lastDependencies ) ) {
- lastBlockSourceDependencies.set( source, dependencies );
-
- // Allow the loop to continue in order to assign latest
- // dependencies values, but mark for reset.
- reset = true;
- }
- }
-
- if ( reset ) {
- yield resetEditorBlocks( yield select( STORE_KEY, 'getEditorBlocks' ), { __unstableShouldCreateUndoLevel: false } );
- }
- }
-}
-
/**
* Returns an action object used in signalling that the latest version of the
* post has been received, either by initialization or save.
@@ -793,44 +627,7 @@ export function unlockPostSaving( lockName ) {
* @yield {Object} Action object
*/
export function* resetEditorBlocks( blocks, options = {} ) {
- const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributeChanges' );
-
- // Sync to sources from block attributes updates.
- if ( lastBlockAttributesChange ) {
- const updatedSources = new Set;
- const updatedBlockTypes = new Set;
- for ( const [ clientId, attributes ] of Object.entries( lastBlockAttributesChange ) ) {
- const blockName = yield select( 'core/block-editor', 'getBlockName', clientId );
- if ( updatedBlockTypes.has( blockName ) ) {
- continue;
- }
-
- updatedBlockTypes.add( blockName );
- const blockType = yield select( 'core/blocks', 'getBlockType', blockName );
-
- for ( const [ attributeName, newAttributeValue ] of Object.entries( attributes ) ) {
- if ( ! blockType.attributes.hasOwnProperty( attributeName ) ) {
- continue;
- }
-
- const schema = blockType.attributes[ attributeName ];
- const source = sources[ schema.source ];
-
- if ( source && source.update ) {
- yield* source.update( schema, newAttributeValue );
- updatedSources.add( source );
- }
- }
- }
-
- // Dependencies are reset so that source dependencies subscription
- // skips a reset which would otherwise occur by dependencies change.
- // This assures that at most one reset occurs per block change.
- yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) );
- }
-
- const edits = { blocks: yield* getBlocksWithSourcedAttributes( blocks ) };
-
+ const edits = { blocks };
if ( options.__unstableShouldCreateUndoLevel !== false ) {
// We create a new function here on every persistent edit
// to make sure the edit makes the post dirty and creates
@@ -838,7 +635,6 @@ export function* resetEditorBlocks( blocks, options = {} ) {
edits.content = ( { blocks: blocksForSerialization = [] } ) =>
serializeBlocks( blocksForSerialization );
}
-
yield* editPost( edits );
}
diff --git a/packages/editor/src/store/block-sources/README.md b/packages/editor/src/store/block-sources/README.md
deleted file mode 100644
index 0c16d12b3159d2..00000000000000
--- a/packages/editor/src/store/block-sources/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-Block Sources
-=============
-
-By default, the blocks module supports only attributes serialized into a block's comment demarcations, or those sourced from a [standard set of sources](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/). Since the blocks module is intended to be used in a number of contexts outside the post editor, the implementation of additional context-specific sources must be implemented as an external process.
-
-The post editor supports such additional sources for attributes (e.g. `meta` source).
-
-These sources are implemented here using a uniform interface for applying and responding to block updates to sourced attributes. In the future, this interface may be generalized to allow third-party extensions to either extend the post editor sources or implement their own in custom renderings of a block editor.
-
-## Source API
-
-### `getDependencies`
-
-Store control called on every store change, expected to return an object whose values represent the data blocks assigned this source depend on. When these values change, all blocks assigned this source are automatically updated. The value returned from this function is passed as the second argument of the source's `apply` function, where it is expected to be used as shared data relevant for sourcing the attribute value.
-
-### `apply`
-
-Function called to retrieve an attribute value for a block. Given the attribute schema and the dependencies defined by the source's `getDependencies`, the function should return the expected attribute value.
-
-### `update`
-
-Store control called when a single block's attributes have been updated, before the new block value has taken effect (i.e. before `apply` and `applyAll` are once again called). Given the attribute schema and updated value, the control should reflect the update on the source.
diff --git a/packages/editor/src/store/block-sources/__mocks__/index.js b/packages/editor/src/store/block-sources/__mocks__/index.js
deleted file mode 100644
index cb0ff5c3b541f6..00000000000000
--- a/packages/editor/src/store/block-sources/__mocks__/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export {};
diff --git a/packages/editor/src/store/block-sources/index.js b/packages/editor/src/store/block-sources/index.js
deleted file mode 100644
index 542d774c313ce9..00000000000000
--- a/packages/editor/src/store/block-sources/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Internal dependencies
- */
-import * as meta from './meta';
-
-export { meta };
diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js
deleted file mode 100644
index 3910395c4a740d..00000000000000
--- a/packages/editor/src/store/block-sources/meta.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { select } from '@wordpress/data-controls';
-
-/**
- * Internal dependencies
- */
-import { editPost } from '../actions';
-
-/**
- * Store control invoked upon a state change, 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.
- *
- * @yield {Object} Optional yielded controls.
- *
- * @return {Object} Dependencies as object.
- */
-export function* getDependencies() {
- return {
- meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ),
- };
-}
-
-/**
- * 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 ];
-}
-
-/**
- * Store control invoked upon a block attributes update, responsible for
- * reflecting an update in a meta value.
- *
- * @param {Object} schema Block type attribute schema.
- * @param {*} value Updated block attribute value.
- *
- * @yield {Object} Yielded action objects or store controls.
- */
-export function* update( schema, value ) {
- yield editPost( {
- meta: {
- [ schema.meta ]: value,
- },
- } );
-}
diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js
index 3c34195bb35415..255c0ea84af397 100644
--- a/packages/editor/src/store/test/actions.js
+++ b/packages/editor/src/store/test/actions.js
@@ -14,7 +14,6 @@ import {
} from '../constants';
jest.mock( '@wordpress/data-controls' );
-jest.mock( '../block-sources' );
select.mockImplementation( ( ...args ) => {
const { select: actualSelect } = jest