diff --git a/docs/manifest.json b/docs/manifest.json
index cf30aff19c503b..3eaf97c31ef344 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -1397,6 +1397,12 @@
"markdown_source": "../packages/base-styles/README.md",
"parent": "packages"
},
+ {
+ "title": "@wordpress/bindings",
+ "slug": "packages-bindings",
+ "markdown_source": "../packages/bindings/README.md",
+ "parent": "packages"
+ },
{
"title": "@wordpress/blob",
"slug": "packages-blob",
diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index 486fcddfe04ac6..16954b90e2479d 100644
--- a/docs/reference-guides/data/data-core-block-editor.md
+++ b/docs/reference-guides/data/data-core-block-editor.md
@@ -1555,6 +1555,36 @@ _Returns_
- `Object`: Action object.
+### resetBindingBlocks
+
+Creates an action to register a block's attribute binding to an external property.
+
+It registers a specific block attribute to be updated based on the value of an external property, identified by a unique key.
+
+_Parameters_
+
+- _clientId_ `string`: - Block client ID.
+- _attribute_ `string`: - The name of the block attribute to bind.
+- _key_ `string`: - The key representing the external property.
+
+_Returns_
+
+- `Object`: Redux 'RESET_BINDING_CONNECTION_BLOCKS' type action.
+
+### resetBlockBindingConnections
+
+Connect blocks with bound attributes to external data sources.
+
+Attributes specified in block metadata bindings are updated according to the corresponding external values.
+
+_Parameters_
+
+- _blocks_ `Object`: - Blocks list.
+
+_Returns_
+
+- `Function`: Returns a Redux thunk function that processes blocks and sets up bindings.
+
### resetBlocks
Action that resets blocks state to the specified array of blocks, taking precedence over any other content reflected as an edit in state.
@@ -1796,6 +1826,10 @@ _Returns_
- `Object`: Action object.
+### unregisterBlockBinding
+
+Undocumented declaration.
+
### unsetBlockEditingMode
Clears the block editing mode for a given block.
@@ -1839,6 +1873,10 @@ _Returns_
- `Object`: Action object.
+### updateBlockBoundAttributes
+
+Undocumented declaration.
+
### updateBlockListSettings
Action that changes the nested settings of a given block.
diff --git a/package-lock.json b/package-lock.json
index 5a23420f5d8884..9f78e9a127ed78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@wordpress/annotations": "file:packages/annotations",
"@wordpress/api-fetch": "file:packages/api-fetch",
"@wordpress/autop": "file:packages/autop",
+ "@wordpress/bindings": "file:packages/bindings",
"@wordpress/blob": "file:packages/blob",
"@wordpress/block-directory": "file:packages/block-directory",
"@wordpress/block-editor": "file:packages/block-editor",
@@ -19131,6 +19132,10 @@
"resolved": "packages/base-styles",
"link": true
},
+ "node_modules/@wordpress/bindings": {
+ "resolved": "packages/bindings",
+ "link": true
+ },
"node_modules/@wordpress/blob": {
"resolved": "packages/blob",
"link": true
@@ -54785,6 +54790,22 @@
"dev": true,
"license": "GPL-2.0-or-later"
},
+ "packages/bindings": {
+ "version": "0.23.0",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/data": "file:../data",
+ "@wordpress/private-apis": "file:../private-apis"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"packages/blob": {
"name": "@wordpress/blob",
"version": "3.54.0",
@@ -54842,6 +54863,7 @@
"@react-spring/web": "^9.4.5",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/commands": "file:../commands",
@@ -54936,6 +54958,7 @@
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/autop": "file:../autop",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
@@ -55819,6 +55842,7 @@
"@babel/runtime": "^7.16.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
@@ -70826,6 +70850,14 @@
"@wordpress/base-styles": {
"version": "file:packages/base-styles"
},
+ "@wordpress/bindings": {
+ "version": "file:packages/bindings",
+ "requires": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/data": "file:../data",
+ "@wordpress/private-apis": "file:../private-apis"
+ }
+ },
"@wordpress/blob": {
"version": "file:packages/blob",
"requires": {
@@ -70867,6 +70899,7 @@
"@react-spring/web": "^9.4.5",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/commands": "file:../commands",
@@ -70934,6 +70967,7 @@
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/autop": "file:../autop",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
@@ -71575,6 +71609,7 @@
"@babel/runtime": "^7.16.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
diff --git a/package.json b/package.json
index 5c63beee49d2a1..9968536240074f 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@wordpress/annotations": "file:packages/annotations",
"@wordpress/api-fetch": "file:packages/api-fetch",
"@wordpress/autop": "file:packages/autop",
+ "@wordpress/bindings": "file:packages/bindings",
"@wordpress/blob": "file:packages/blob",
"@wordpress/block-directory": "file:packages/block-directory",
"@wordpress/block-editor": "file:packages/block-editor",
diff --git a/packages/bindings/.npmrc b/packages/bindings/.npmrc
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/packages/bindings/CHANGELOG.md b/packages/bindings/CHANGELOG.md
new file mode 100644
index 00000000000000..bdf416b94104e3
--- /dev/null
+++ b/packages/bindings/CHANGELOG.md
@@ -0,0 +1,3 @@
+
+
+Initial release.
diff --git a/packages/bindings/README.md b/packages/bindings/README.md
new file mode 100644
index 00000000000000..b5ef08d96becd7
--- /dev/null
+++ b/packages/bindings/README.md
@@ -0,0 +1 @@
+# Bindings API
diff --git a/packages/bindings/package.json b/packages/bindings/package.json
new file mode 100644
index 00000000000000..430f4a3651ea51
--- /dev/null
+++ b/packages/bindings/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@wordpress/bindings",
+ "version": "0.23.0",
+ "description": "Connect to external sources by using the Bindings API.",
+ "author": "The WordPress Contributors",
+ "license": "GPL-2.0-or-later",
+ "keywords": [
+ "wordpress",
+ "gutenberg",
+ "bindings"
+ ],
+ "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/bindings/README.md",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/WordPress/gutenberg.git",
+ "directory": "packages/bindings"
+ },
+ "bugs": {
+ "url": "https://github.com/WordPress/gutenberg/issues"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "main": "build/index.js",
+ "module": "build-module/index.js",
+ "react-native": "src/index",
+ "dependencies": {
+ "@babel/runtime": "^7.16.0",
+ "@wordpress/data": "file:../data",
+ "@wordpress/private-apis": "file:../private-apis"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/bindings/src/index.js b/packages/bindings/src/index.js
new file mode 100644
index 00000000000000..33b78d5a1d117e
--- /dev/null
+++ b/packages/bindings/src/index.js
@@ -0,0 +1 @@
+export { store } from './store';
diff --git a/packages/bindings/src/lock-unlock.js b/packages/bindings/src/lock-unlock.js
new file mode 100644
index 00000000000000..e11bd687d87421
--- /dev/null
+++ b/packages/bindings/src/lock-unlock.js
@@ -0,0 +1,10 @@
+/**
+ * WordPress dependencies
+ */
+import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
+
+export const { lock, unlock } =
+ __dangerousOptInToUnstableAPIsOnlyForCoreModules(
+ 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',
+ '@wordpress/commands'
+ );
diff --git a/packages/bindings/src/store/index.js b/packages/bindings/src/store/index.js
new file mode 100644
index 00000000000000..adf76c89d62655
--- /dev/null
+++ b/packages/bindings/src/store/index.js
@@ -0,0 +1,37 @@
+/**
+ * WordPress dependencies
+ */
+import { createReduxStore, register } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import reducer from './reducer';
+import * as privateActions from './private-actions';
+import * as privateSelectors from './private-selectors';
+import { unlock } from '../lock-unlock';
+
+const STORE_NAME = 'core/bindings';
+
+/**
+ * Store definition for the bindings namespace.
+ *
+ * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore
+ *
+ * @type {Object}
+ *
+ * @example
+ * ```js
+ * import { store as bindingsStore } from '@wordpress/bindings';
+ * import { dispatch } from '@wordpress/data';
+ *
+ * const { registerBindingsSource } = dispatch( bindingsStore );
+ * ```
+ */
+export const store = createReduxStore( STORE_NAME, {
+ reducer,
+} );
+
+register( store );
+unlock( store ).registerPrivateActions( privateActions );
+unlock( store ).registerPrivateSelectors( privateSelectors );
diff --git a/packages/bindings/src/store/private-actions.js b/packages/bindings/src/store/private-actions.js
new file mode 100644
index 00000000000000..72cf1a3229a3b1
--- /dev/null
+++ b/packages/bindings/src/store/private-actions.js
@@ -0,0 +1,107 @@
+/**
+ * Internal dependencies
+ */
+import { generateBindingsConnectionKey } from './utils';
+
+/**
+ * Register an external source
+ *
+ * @param {string} source Name of the source to register.
+ */
+export function registerBindingsSource( source ) {
+ return {
+ type: 'REGISTER_BINDINGS_SOURCE',
+ ...source,
+ };
+}
+
+/**
+ * Sets up a subscription to monitor changes
+ * in a specified bindings connection.
+ *
+ * @param {string} key - The key identifying the bindings connection to observe.
+ * @param {Function} [updateCallback] - Optional callback invoked when the observed property changes.
+ * @return {Function} Thunk.
+ */
+export function observeBindingsConnectionChange( key, updateCallback ) {
+ return ( { select, registry, dispatch } ) => {
+ const handler = select.getBindingsConnectionHandler( key );
+ let currentValue = handler.get();
+
+ function watchValueChanges() {
+ const value = select.getBindingsConnectionValue( key );
+ if ( value === currentValue ) {
+ return;
+ }
+
+ currentValue = value;
+ updateCallback?.( value );
+ }
+
+ dispatch( {
+ type: 'UPDATE_BINDINGS_CONNECTION',
+ key,
+ unsubscribe: registry.subscribe( watchValueChanges ),
+ } );
+ };
+}
+
+/**
+ * Registers a connection to a bindings source.
+ * The connection is established by a combination
+ * of the source handler name and the binding arguments.
+ *
+ * @param {Object} settings - Settings.
+ * @param {string} settings.source - The source handler name.
+ * @param {Object} settings.args - The binding arguments.
+ * @param {Function} updateCallback - Callback invoked when the connection value changes.
+ * @return {Function} Thunk function for Redux dispatch, registers the connection.
+ */
+export function registerBindingsConnection( { source, args }, updateCallback ) {
+ return ( { dispatch, select } ) => {
+ const settings = select.getBindingsSource( source );
+ if ( ! settings ) {
+ return;
+ }
+
+ // Do not register if it's already registered.
+ const key = generateBindingsConnectionKey( { source, args } );
+ if ( select.getBindingsConnectionHandler( key ) ) {
+ return;
+ }
+
+ const { connect } = settings;
+ const handler = connect( args );
+
+ dispatch( {
+ type: 'REGISTER_BINDINGS_CONNECTION',
+ key,
+ ...handler,
+ } );
+
+ /*
+ * Observe the external property to monitor changes.
+ * To do: scale this to register multiple callbacks.
+ */
+ dispatch.observeBindingsConnectionChange( key, updateCallback );
+ };
+}
+
+/**
+ * Updates the value of a bindings connection.
+ * The connection is identified by the connection key.
+ *
+ * @param {string} key - The connection key.
+ * @param {*} value - The new value.
+ * @return {Function} Thunk function for Redux dispatch, updates the connection value.
+ */
+export function updateBindingsConnectionValue( key, value ) {
+ return ( { select } ) => {
+ const bindingsConnection = select.getBindingsConnectionHandler( key );
+ if ( ! bindingsConnection ) {
+ return;
+ }
+
+ bindingsConnection.update( value );
+ };
+}
diff --git a/packages/bindings/src/store/private-selectors.js b/packages/bindings/src/store/private-selectors.js
new file mode 100644
index 00000000000000..193d2b9e45684d
--- /dev/null
+++ b/packages/bindings/src/store/private-selectors.js
@@ -0,0 +1,63 @@
+/**
+ * Internal dependencies
+ */
+import { generateBindingsConnectionKey } from './utils';
+
+/**
+ * Returns all the bindings sources registered.
+ *
+ * @param {Object} state - Data state.
+ * @return {Object} All registered sources handlers.
+ */
+export function getAllBindingsSources( state ) {
+ return state.sources;
+}
+
+/**
+ * Returns a specific bindings source.
+ *
+ * @param {Object} state - Data state.
+ * @param {string} sourceName - Source handler name.
+ * @return {Object} The specific binding source.
+ */
+export function getBindingsSource( state, sourceName ) {
+ return state.sources[ sourceName ];
+}
+
+/**
+ * Return the bindings connection key,
+ * based on the source handler name and the binding arguments.
+ *
+ * @param {Object} state - Data state.
+ * @param {Object} settings - Settings.
+ * @param {string} settings.source - The source handler name.
+ * @param {Object} settings.args - The binding arguments.
+ * @return {string|undefined} The generated key.
+ */
+export function getBindingsConnectionKey( state, settings ) {
+ return generateBindingsConnectionKey( settings );
+}
+
+/**
+ * Return the bindings connection handler,
+ * based on the connection key.
+ *
+ * @param {Object} state - Data state.
+ * @param {string} key - The connection key.
+ * @return {Object} The connection handler.
+ */
+export function getBindingsConnectionHandler( state, key ) {
+ return state.connections?.[ key ];
+}
+
+/**
+ * Return the bindings connection value,
+ * based on the connection key.
+ *
+ * @param {Object} state - Data state.
+ * @param {string} key - The connection key.
+ * @return {*} The connection value.
+ */
+export function getBindingsConnectionValue( state, key ) {
+ return getBindingsConnectionHandler( state, key )?.get();
+}
diff --git a/packages/bindings/src/store/reducer.js b/packages/bindings/src/store/reducer.js
new file mode 100644
index 00000000000000..608bc7c1e30f3c
--- /dev/null
+++ b/packages/bindings/src/store/reducer.js
@@ -0,0 +1,53 @@
+/**
+ * WordPress dependencies
+ */
+import { combineReducers } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+
+export function sources( state = {}, action ) {
+ if ( action.type === 'REGISTER_BINDINGS_SOURCE' ) {
+ const source = { ...action };
+ delete source.type;
+ delete source.name;
+
+ return {
+ ...state,
+ [ action.name ]: source,
+ };
+ }
+ return state;
+}
+
+export function connections( state = {}, action ) {
+ switch ( action.type ) {
+ case 'REGISTER_BINDINGS_CONNECTION': {
+ const { key, type, ...rest } = action;
+ return {
+ ...state,
+ [ key ]: { ...rest },
+ };
+ }
+
+ case 'UPDATE_BINDINGS_CONNECTION': {
+ const { type, key, ...updates } = action;
+ return {
+ ...state,
+ [ key ]: {
+ ...state[ key ],
+ ...updates,
+ },
+ };
+ }
+ }
+
+ return state;
+}
+
+const reducer = combineReducers( {
+ sources,
+ connections,
+} );
+
+export default reducer;
diff --git a/packages/bindings/src/store/utils.js b/packages/bindings/src/store/utils.js
new file mode 100644
index 00000000000000..64db5a9cdc00d9
--- /dev/null
+++ b/packages/bindings/src/store/utils.js
@@ -0,0 +1,16 @@
+/**
+ * Generates a key for a bindings connection,
+ * based on the source handler name and the binding arguments.
+ *
+ * @param {Object} settings - Settings.
+ * @param {string} settings.source - The source handler name.
+ * @param {Object} settings.args - The binding arguments.
+ * @return {string|undefined} The generated key.
+ */
+export function generateBindingsConnectionKey( { source, args } = {} ) {
+ if ( ! source?.length || ! args ) {
+ return;
+ }
+
+ return `${ source }|${ JSON.stringify( args ).replace( /{|}|"/g, '' ) }`;
+}
diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json
index 498eee0c936017..b0fe8dea729397 100644
--- a/packages/block-editor/package.json
+++ b/packages/block-editor/package.json
@@ -37,6 +37,7 @@
"@react-spring/web": "^9.4.5",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/commands": "file:../commands",
diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js
index c929c1014dc030..0df7170539d51e 100644
--- a/packages/block-editor/src/components/block-list/use-block-props/index.js
+++ b/packages/block-editor/src/components/block-list/use-block-props/index.js
@@ -29,7 +29,7 @@ import { useNavModeExit } from './use-nav-mode-exit';
import { useBlockRefProvider } from './use-block-refs';
import { useIntersectionObserver } from './use-intersection-observer';
import { useFlashEditableBlocks } from '../../use-flash-editable-blocks';
-import { canBindBlock } from '../../../hooks/use-bindings-attributes';
+import { canBindBlock } from '../../../../../editor/src/bindings/utils';
/**
* This hook is used to lightly mark an element as a block element. The element
diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js
index 8d28bd001670fc..b64c7cdfbc1a8a 100644
--- a/packages/block-editor/src/components/block-toolbar/index.js
+++ b/packages/block-editor/src/components/block-toolbar/index.js
@@ -37,7 +37,7 @@ import NavigableToolbar from '../navigable-toolbar';
import Shuffle from './shuffle';
import BlockBindingsIndicator from '../block-bindings-toolbar-indicator';
import { useHasBlockToolbar } from './use-has-block-toolbar';
-import { canBindBlock } from '../../hooks/use-bindings-attributes';
+import { canBindBlock } from '../../../../editor/src/bindings/utils';
/**
* Renders the block toolbar.
*
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index 3d1725c54fe715..65ec7e6be36cf5 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -3,6 +3,11 @@
*/
import classnames from 'classnames';
+/**
+ * WordPress dependencies
+ */
+import { store as bindingsStore } from '@wordpress/bindings';
+
/**
* WordPress dependencies
*/
@@ -19,7 +24,7 @@ import {
removeFormat,
} from '@wordpress/rich-text';
import { Popover } from '@wordpress/components';
-import { getBlockType, store as blocksStore } from '@wordpress/blocks';
+import { getBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -47,7 +52,7 @@ import { getAllowedFormats } from './utils';
import { Content } from './content';
import { withDeprecations } from './with-deprecations';
import { unlock } from '../../lock-unlock';
-import { canBindBlock } from '../../hooks/use-bindings-attributes';
+import { canBindBlock } from '../../../../editor/src/bindings/utils';
export const keyboardShortcutContext = createContext();
export const inputEventContext = createContext();
@@ -164,9 +169,7 @@ export function RichTextWrapper(
if ( blockBindings && canBindBlock( blockName ) ) {
const blockTypeAttributes =
getBlockType( blockName ).attributes;
- const { getBlockBindingsSource } = unlock(
- select( blocksStore )
- );
+ const { getBindingsSource } = unlock( select( bindingsStore ) );
for ( const [ attribute, args ] of Object.entries(
blockBindings
) ) {
@@ -178,9 +181,10 @@ export function RichTextWrapper(
}
// If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it.
- const blockBindingsSource = getBlockBindingsSource(
+ const blockBindingsSource = getBindingsSource(
args.source
);
+
if (
! blockBindingsSource ||
blockBindingsSource.lockAttributesEditing
@@ -329,7 +333,7 @@ export function RichTextWrapper(
onChange,
} );
- useMarkPersistent( { html: adjustedValue, value } );
+ useMarkPersistent( value );
const keyboardShortcuts = useRef( new Set() );
const inputEvents = useRef( new Set() );
diff --git a/packages/block-editor/src/components/rich-text/use-mark-persistent.js b/packages/block-editor/src/components/rich-text/use-mark-persistent.js
index 10e157452fbe22..e52df535a84161 100644
--- a/packages/block-editor/src/components/rich-text/use-mark-persistent.js
+++ b/packages/block-editor/src/components/rich-text/use-mark-persistent.js
@@ -9,7 +9,7 @@ import { useDispatch } from '@wordpress/data';
*/
import { store as blockEditorStore } from '../../store';
-export function useMarkPersistent( { html, value } ) {
+export function useMarkPersistent( value ) {
const previousText = useRef();
const hasActiveFormats = !! value.activeFormats?.length;
const { __unstableMarkLastChangeAsPersistent } =
@@ -36,5 +36,5 @@ export function useMarkPersistent( { html, value } ) {
}
__unstableMarkLastChangeAsPersistent();
- }, [ html, hasActiveFormats ] );
+ }, [ value.text, hasActiveFormats ] );
}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index 5773cd7c34595b..95809106cb2f63 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -29,7 +29,6 @@ import contentLockUI from './content-lock-ui';
import './metadata';
import blockHooks from './block-hooks';
import blockRenaming from './block-renaming';
-import './use-bindings-attributes';
createBlockEditFilter(
[
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
deleted file mode 100644
index 5cd8cb46b3b7e7..00000000000000
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ /dev/null
@@ -1,264 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { getBlockType, store as blocksStore } from '@wordpress/blocks';
-import { createHigherOrderComponent } from '@wordpress/compose';
-import { useSelect } from '@wordpress/data';
-import { useLayoutEffect, useCallback, useState } from '@wordpress/element';
-import { addFilter } from '@wordpress/hooks';
-import { RichTextData } from '@wordpress/rich-text';
-
-/**
- * Internal dependencies
- */
-import { unlock } from '../lock-unlock';
-
-/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */
-/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */
-
-/**
- * Given a binding of block attributes, returns a higher order component that
- * overrides its `attributes` and `setAttributes` props to sync any changes needed.
- *
- * @return {WPHigherOrderComponent} Higher-order component.
- */
-
-const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
- 'core/paragraph': [ 'content' ],
- 'core/heading': [ 'content' ],
- 'core/image': [ 'url', 'title', 'alt' ],
- 'core/button': [ 'url', 'text', 'linkTarget' ],
-};
-
-/**
- * Based on the given block name,
- * check if it is possible to bind the block.
- *
- * @param {string} blockName - The block name.
- * @return {boolean} Whether it is possible to bind the block to sources.
- */
-export function canBindBlock( blockName ) {
- return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
-}
-
-/**
- * Based on the given block name and attribute name,
- * check if it is possible to bind the block attribute.
- *
- * @param {string} blockName - The block name.
- * @param {string} attributeName - The attribute name.
- * @return {boolean} Whether it is possible to bind the block attribute.
- */
-export function canBindAttribute( blockName, attributeName ) {
- return (
- canBindBlock( blockName ) &&
- BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
- );
-}
-
-/**
- * This component is responsible for detecting and
- * propagating data changes from the source to the block.
- *
- * @param {Object} props - The component props.
- * @param {string} props.attrName - The attribute name.
- * @param {Object} props.blockProps - The block props with bound attribute.
- * @param {Object} props.source - Source handler.
- * @param {Object} props.args - The arguments to pass to the source.
- * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
- * @return {null} Data-handling component. Render nothing.
- */
-const BindingConnector = ( {
- args,
- attrName,
- blockProps,
- source,
- onPropValueChange,
-} ) => {
- const { placeholder, value: propValue } = source.useSource(
- blockProps,
- args
- );
-
- const { name: blockName } = blockProps;
- const attrValue = blockProps.attributes[ attrName ];
-
- const updateBoundAttibute = useCallback(
- ( newAttrValue, prevAttrValue ) => {
- /*
- * If the attribute is a RichTextData instance,
- * (core/paragraph, core/heading, core/button, etc.)
- * compare its HTML representation with the new value.
- *
- * To do: it looks like a workaround.
- * Consider improving the attribute and metadata fields types.
- */
- if ( prevAttrValue instanceof RichTextData ) {
- // Bail early if the Rich Text value is the same.
- if ( prevAttrValue.toHTMLString() === newAttrValue ) {
- return;
- }
-
- /*
- * To preserve the value type,
- * convert the new value to a RichTextData instance.
- */
- newAttrValue = RichTextData.fromHTMLString( newAttrValue );
- }
-
- if ( prevAttrValue === newAttrValue ) {
- return;
- }
-
- onPropValueChange( { [ attrName ]: newAttrValue } );
- },
- [ attrName, onPropValueChange ]
- );
-
- useLayoutEffect( () => {
- if ( typeof propValue !== 'undefined' ) {
- updateBoundAttibute( propValue, attrValue );
- } else if ( placeholder ) {
- /*
- * Placeholder fallback.
- * If the attribute is `src` or `href`,
- * a placeholder can't be used because it is not a valid url.
- * Adding this workaround until
- * attributes and metadata fields types are improved and include `url`.
- */
- const htmlAttribute =
- getBlockType( blockName ).attributes[ attrName ].attribute;
-
- if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) {
- updateBoundAttibute( null );
- return;
- }
-
- updateBoundAttibute( placeholder );
- }
- }, [
- updateBoundAttibute,
- propValue,
- attrValue,
- placeholder,
- blockName,
- attrName,
- ] );
-
- return null;
-};
-
-/**
- * BlockBindingBridge acts like a component wrapper
- * that connects the bound attributes of a block
- * to the source handlers.
- * For this, it creates a BindingConnector for each bound attribute.
- *
- * @param {Object} props - The component props.
- * @param {Object} props.blockProps - The BlockEdit props object.
- * @param {Object} props.bindings - The block bindings settings.
- * @param {Function} props.onPropValueChange - The function to call when the attribute value changes.
- * @return {null} Data-handling component. Render nothing.
- */
-function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) {
- const blockBindingsSources = unlock(
- useSelect( blocksStore )
- ).getAllBlockBindingsSources();
-
- return (
- <>
- { Object.entries( bindings ).map(
- ( [ attrName, boundAttribute ] ) => {
- // Bail early if the block doesn't have a valid source handler.
- const source =
- blockBindingsSources[ boundAttribute.source ];
- if ( ! source?.useSource ) {
- return null;
- }
-
- return (
-
- );
- }
- ) }
- >
- );
-}
-
-const withBlockBindingSupport = createHigherOrderComponent(
- ( BlockEdit ) => ( props ) => {
- /*
- * Collect and update the bound attributes
- * in a separate state.
- */
- const [ boundAttributes, setBoundAttributes ] = useState( {} );
- const updateBoundAttributes = useCallback(
- ( newAttributes ) =>
- setBoundAttributes( ( prev ) => ( {
- ...prev,
- ...newAttributes,
- } ) ),
- []
- );
-
- /*
- * Create binding object filtering
- * only the attributes that can be bound.
- */
- const bindings = Object.fromEntries(
- Object.entries( props.attributes.metadata?.bindings || {} ).filter(
- ( [ attrName ] ) => canBindAttribute( props.name, attrName )
- )
- );
-
- return (
- <>
- { Object.keys( bindings ).length > 0 && (
-
- ) }
-
-
- >
- );
- },
- 'withBlockBindingSupport'
-);
-
-/**
- * Filters a registered block's settings to enhance a block's `edit` component
- * to upgrade bound attributes.
- *
- * @param {WPBlockSettings} settings - Registered block settings.
- * @param {string} name - Block name.
- * @return {WPBlockSettings} Filtered block settings.
- */
-function shimAttributeSource( settings, name ) {
- if ( ! canBindBlock( name ) ) {
- return settings;
- }
-
- return {
- ...settings,
- edit: withBlockBindingSupport( settings.edit ),
- };
-}
-
-addFilter(
- 'blocks.registerBlockType',
- 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source',
- shimAttributeSource
-);
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index e9281727804f1c..643f7562fc3363 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -15,6 +15,7 @@ import {
getBlockSupport,
isUnmodifiedDefaultBlock,
} from '@wordpress/blocks';
+import { store as bindingsStore } from '@wordpress/bindings';
import { speak } from '@wordpress/a11y';
import { __, _n, sprintf } from '@wordpress/i18n';
import { create, insert, remove, toHTMLString } from '@wordpress/rich-text';
@@ -31,6 +32,7 @@ import {
__experimentalUpdateSettings,
privateRemoveBlocks,
} from './private-actions';
+import { unlock } from '../lock-unlock';
/** @typedef {import('../components/use-on-block-drop/types').WPDropOperation} WPDropOperation */
@@ -47,6 +49,7 @@ export const resetBlocks =
( blocks ) =>
( { dispatch } ) => {
dispatch( { type: 'RESET_BLOCKS', blocks } );
+ dispatch( resetBlockBindingConnections( blocks ) );
dispatch( validateBlocksToTemplate( blocks ) );
};
@@ -148,6 +151,60 @@ export function receiveBlocks( blocks ) {
};
}
+export function updateBlockBoundAttributes(
+ clientIds,
+ attributes,
+ uniqueByBlock = false
+) {
+ clientIds = castArray( clientIds );
+
+ return ( { dispatch, select, registry } ) => {
+ for ( const clientId of clientIds ) {
+ for ( const [ attribute, value ] of Object.entries( attributes ) ) {
+ /*
+ * Get the external property key bound to the attribute,
+ * based on the block client ID and attribute name.
+ */
+ const propertyKey = select.getBindingsConnectionKey(
+ clientId,
+ attribute
+ );
+
+ /*
+ * Get all blocks that have an attribute
+ * bound to the same external property.
+ */
+ const blocksWithBoundAttributes =
+ select.getBlocksByBindingsConnectionKey(
+ propertyKey,
+ value
+ );
+
+ if ( ! blocksWithBoundAttributes?.length ) {
+ continue;
+ }
+
+ /*
+ * Update all blocks that have
+ * an attribute bound to the same external property.
+ */
+ blocksWithBoundAttributes.forEach( ( subAction ) => {
+ registry.batch( () => {
+ dispatch.syncDerivedUpdates( () => {
+ dispatch( {
+ type: 'UPDATE_BLOCK_ATTRIBUTES',
+ clientIds: subAction.clientIds,
+ attributes: subAction.attributes,
+ uniqueByBlock,
+ } );
+ } );
+ } );
+ } );
+ }
+ }
+ };
+}
+
/**
* Action that updates attributes of multiple blocks with the specified client IDs.
*
@@ -162,11 +219,170 @@ export function updateBlockAttributes(
attributes,
uniqueByBlock = false
) {
+ clientIds = castArray( clientIds );
+
+ return ( { dispatch, select, registry } ) => {
+ dispatch( {
+ type: 'UPDATE_BLOCK_ATTRIBUTES',
+ clientIds,
+ attributes,
+ uniqueByBlock,
+ } );
+
+ const { updateBindingsConnectionValue } = unlock(
+ registry.dispatch( bindingsStore )
+ );
+
+ registry.batch( () => {
+ for ( const clientId of clientIds ) {
+ for ( const [ attribute, value ] of Object.entries(
+ attributes
+ ) ) {
+ // Pick the external property key bound to the attribute.
+ const key = select.getBindingsConnectionKey(
+ clientId,
+ attribute
+ );
+
+ /*
+ * Update the external property with the new value.
+ */
+ updateBindingsConnectionValue( key, value );
+ }
+ }
+ } );
+ };
+}
+
+/**
+ * Connect blocks with bound attributes to external data sources.
+ *
+ * Attributes specified in block metadata bindings
+ * are updated according to the corresponding external values.
+ *
+ * @param {Object} blocks - Blocks list.
+ * @return {Function} Returns a Redux thunk function that processes blocks and sets up bindings.
+ */
+export function resetBlockBindingConnections( blocks ) {
+ return ( { dispatch, registry, select } ) => {
+ const clientIdsWithBoundAttributes =
+ select.getBlocksWithBoundAttributes( blocks );
+
+ if ( ! clientIdsWithBoundAttributes ) {
+ return;
+ }
+
+ const { registerBindingsConnection } = unlock(
+ registry.dispatch( bindingsStore )
+ );
+
+ const { getBindingsConnectionKey } = unlock(
+ registry.select( bindingsStore )
+ );
+
+ registry.batch( () => {
+ Object.entries( clientIdsWithBoundAttributes ).forEach(
+ ( [ clientId, attributes ] ) => {
+ Object.entries( attributes ).forEach(
+ ( [ attribute, bindSettings ] ) => {
+ /*
+ * Register the external property handler,
+ * and pass a callback function
+ * to update the value of the bound attribute.
+ */
+ registerBindingsConnection(
+ bindSettings,
+ ( newValue ) => {
+ /*
+ * [binding-on-sync]: Update bound attribute value.
+ *
+ * Update the block attribute
+ * when the external property value changes.
+ */
+ dispatch.syncDerivedUpdates( () => {
+ dispatch.updateBlockBoundAttributes(
+ clientId,
+ {
+ [ attribute ]: newValue,
+ }
+ );
+ } );
+ }
+ );
+
+ // Pick the external property key bound to the attribute.
+ const bindPropertyKey =
+ getBindingsConnectionKey( bindSettings );
+
+ dispatch.syncDerivedUpdates( () => {
+ dispatch.resetBindingBlocks(
+ clientId,
+ attribute,
+ bindPropertyKey
+ );
+ } );
+
+ /*
+ * [binding-on-sync]: First bound attribute value update.
+ */
+
+ const currentAttributeValue =
+ select.getBlockAttributes( clientId )?.[
+ attribute
+ ];
+
+ const boundValue = select.getBoundAttributeValue(
+ bindPropertyKey,
+ currentAttributeValue
+ );
+
+ /*
+ * Sync the block attribute with the external property value.
+ */
+ if ( currentAttributeValue !== boundValue ) {
+ dispatch.syncDerivedUpdates( () => {
+ dispatch( {
+ type: 'UPDATE_BLOCK_ATTRIBUTES',
+ clientIds: [ clientId ],
+ attributes: {
+ [ attribute ]: boundValue,
+ },
+ uniqueByBlock: false,
+ } );
+ } );
+ }
+ }
+ );
+ }
+ );
+ } );
+ };
+}
+
+/**
+ * Creates an action to register a block's attribute binding to an external property.
+ *
+ * It registers a specific block attribute to be updated based on the value of an external property,
+ * identified by a unique key.
+ *
+ * @param {string} clientId - Block client ID.
+ * @param {string} attribute - The name of the block attribute to bind.
+ * @param {string} key - The key representing the external property.
+ * @return {Object} Redux 'RESET_BINDING_CONNECTION_BLOCKS' type action.
+ */
+export function resetBindingBlocks( clientId, attribute, key ) {
+ return {
+ type: 'RESET_BINDING_CONNECTION_BLOCKS',
+ clientId,
+ attribute,
+ key,
+ };
+}
+export function unregisterBlockBinding( clientId, attribute ) {
return {
- type: 'UPDATE_BLOCK_ATTRIBUTES',
- clientIds: castArray( clientIds ),
- attributes,
- uniqueByBlock,
+ type: 'UNRESET_BINDING_CONNECTION_BLOCKS',
+ clientId,
+ attribute,
};
}
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index e4885cbbd9e1e1..0398e1cab65d7a 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -7,6 +7,7 @@ import createSelector from 'rememo';
* WordPress dependencies
*/
import { createRegistrySelector } from '@wordpress/data';
+import { store as bindingsStore } from '@wordpress/bindings';
/**
* Internal dependencies
@@ -23,6 +24,11 @@ import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-ta
import { STORE_NAME } from './constants';
import { unlock } from '../lock-unlock';
import { selectBlockPatternsKey } from './private-keys';
+import { RichTextData } from '@wordpress/rich-text';
+import {
+ canBindAttribute,
+ canBindBlock,
+} from '../../../editor/src/bindings/utils';
export { getBlockSettings } from './get-block-settings';
@@ -353,3 +359,130 @@ export function getLastFocus( state ) {
export function isDragging( state ) {
return state.isDragging;
}
+
+/**
+ * Return the bindings connection key
+ * for a given block client ID and attribute.
+ *
+ * @param {Object} state - Global application state.
+ * @param {string} clientId - Block client ID.
+ * @param {string} attribute - Block attribute name.
+ * @return {string} Bingins connection key.
+ */
+export function getBindingsConnectionKey( state, clientId, attribute ) {
+ if ( ! state.bindings ) {
+ return {};
+ }
+
+ return state.bindings?.byClientId.get( clientId )?.connectionKeys[
+ attribute
+ ];
+}
+
+/**
+ * Returns a blocks with bound attributes collection.
+ *
+ * @param {Object} state - Global application state.
+ * @return {Object} Block with bound attributes collection.
+ */
+export function getBlocksWithBoundAttributes( state ) {
+ // If there are no bindings, return an empty object.
+ if ( ! state.bindings?.byClientId ) {
+ return {};
+ }
+
+ const result = {};
+
+ state.bindings.byClientId.forEach( ( block, clientId ) => {
+ if ( ! canBindBlock( block.name ) ) {
+ return;
+ }
+
+ // Check if the attribute can be bound.
+ const boundAttributes = Object.fromEntries(
+ Object.entries( block.attributes || {} ).filter(
+ ( [ attribute ] ) => {
+ return canBindAttribute( block.name, attribute );
+ }
+ )
+ );
+
+ if ( Object.keys( boundAttributes ).length === 0 ) {
+ return;
+ }
+
+ result[ clientId ] = boundAttributes;
+ } );
+
+ return result;
+}
+
+/**
+ * Return a list of blocks that are bound
+ * to the same bindings connection key.
+ *
+ * @param {Object} state - Global application state.
+ * @param {string} key - Bingins connection key.
+ * @param {string} value - Bingins connection value.
+ * @return {Object[]} List of blocks with the same bindings connection key.
+ */
+export function getBlocksByBindingsConnectionKey( state, key, value ) {
+ const bindingsConnection = state.bindings.connections.get( key );
+ if ( ! bindingsConnection ) {
+ return [];
+ }
+
+ return Object.entries( bindingsConnection ).map(
+ ( [ attr, clientIds ] ) => {
+ return {
+ attributes: { [ attr ]: value },
+ clientIds,
+ };
+ }
+ );
+}
+
+/**
+ * Selects and optionally transforms
+ * the value bound to an external property.
+ *
+ * @param {Object} state - Redux state.
+ * @param {string} key - External property key.
+ * @param {string|RichTextData} attribute - Current attribute value.
+ * @return {string|RichTextData} Transformed or original bound attribute.
+ */
+export const getBoundAttributeValue = createRegistrySelector( ( select ) =>
+ createSelector( ( state, key, attribute ) => {
+ const { getBindingsConnectionValue } = unlock(
+ select( bindingsStore )
+ );
+
+ const externalValue = getBindingsConnectionValue( key );
+
+ // Type: string
+ if ( typeof attribute === 'string' ) {
+ return externalValue;
+ }
+
+ // Type: RichTextData
+ if ( attribute instanceof RichTextData ) {
+ /*
+ * Compare the string (HTML) value of the RichTextData
+ * with the external value.
+ *
+ * If they are the same, return the same attribute.
+ */
+ if ( attribute.toHTMLString() === externalValue ) {
+ return attribute;
+ }
+
+ /*
+ * Otherwise, return a RichTextData instance
+ * with the value of the external value.
+ */
+ return RichTextData.fromHTMLString( externalValue );
+ }
+
+ return externalValue;
+ } )
+);
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index e836a44e12012f..2b7a5cc84c9645 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -131,6 +131,26 @@ function getFlattenedBlockAttributes( blocks ) {
return flattenBlocks( blocks, ( block ) => block.attributes );
}
+/**
+ * Given an array of blocks, returns an object containing all blocks
+ * that have bindings, recursing into inner blocks.
+ * Keys correspond to the block client ID, the value contains the following properties:
+ * - name: the block name
+ * - attributes: the block attributes that have bindings.
+ * Keys correspond to the attribute name, the value contains the following properties:
+ * - source: The binding source handler name,
+ * - args: The binding arguments.
+ *
+ * @param {Array} blocks - Blocks to flatten.
+ * @return {Array} Flattened block.
+ */
+function getFlattenedBindingExternalProperties( blocks ) {
+ return flattenBlocks( blocks, ( block ) => ( {
+ name: block.name,
+ attributes: block.attributes?.metadata?.bindings,
+ } ) ).filter( ( [ , bindings ] ) => bindings.attributes !== undefined );
+}
+
/**
* Returns true if the two object arguments have the same keys, or false
* otherwise.
@@ -258,6 +278,7 @@ const withBlockTree =
}
newState.tree = state.tree ? state.tree : new Map();
+
switch ( action.type ) {
case 'RECEIVE_BLOCKS':
case 'INSERT_BLOCKS': {
@@ -2044,7 +2065,95 @@ export function lastFocus( state = false, action ) {
return state;
}
+const withBindingBlockReset = ( reducer ) => ( state, action ) => {
+ if ( action.type === 'RESET_BLOCKS' ) {
+ const newState = {
+ ...state,
+ byClientId: new Map(
+ getFlattenedBindingExternalProperties( action.blocks )
+ ),
+ };
+ return newState;
+ }
+
+ return reducer( state, action );
+};
+
+/**
+ * Reducer returning the block bindings state.
+ */
+export const bindings = pipe(
+ combineReducers,
+ withBindingBlockReset
+)( {
+ byClientId( state = new Map(), action ) {
+ switch ( action.type ) {
+ case 'RESET_BINDING_CONNECTION_BLOCKS': {
+ const { clientId, attribute, key } = action;
+ const newState = new Map( state );
+
+ /*
+ * Populate ClientId entry
+ * with the property key bound to the block attributes.
+ */
+ const currentBindings = newState.get( clientId ) || {};
+ newState.set( clientId, {
+ ...currentBindings,
+ connectionKeys: {
+ ...currentBindings.connectionKeys,
+ [ attribute ]: key,
+ },
+ } );
+
+ return newState;
+ }
+ }
+
+ return state;
+ },
+
+ connections( state = new Map(), action ) {
+ switch ( action.type ) {
+ /*
+ * Collect the blocks with an attribute
+ * bound to the external property,
+ * organized by:
+ * {
+ * [connection-key_1]: {
+ * [attribute_1]: [ clientId_1, clientId_2, ... ],
+ * [attribute_2]: [ clientId_1, clientId_3, ... ],
+ * ...
+ * },
+ * [connection-key_2]: {
+ * [attribute_1]: [ clientId_1, clientId_4, ... ],
+ * [attribute_3]: [ clientId_2, clientId_8, ... ],
+ * ...
+ * },
+ * ...
+ * }
+ */
+ case 'RESET_BINDING_CONNECTION_BLOCKS': {
+ const { clientId, attribute, key } = action;
+ const newState = new Map( state );
+
+ newState.set( key, {
+ ...newState.get( key ),
+ [ attribute ]: [
+ ...( newState.get( key )?.[ attribute ] || [] ),
+ clientId,
+ ],
+ } );
+
+ return newState;
+ }
+ }
+
+ return state;
+ },
+} );
+
const combinedReducers = combineReducers( {
+ bindings,
blocks,
isDragging,
isTyping,
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index 0a024f3a5f422e..215bb2cde1d219 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -34,6 +34,7 @@
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/autop": "file:../autop",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js
index 24f82f1ba2f4f0..0165ef1cc1cb8c 100644
--- a/packages/block-library/src/button/edit.js
+++ b/packages/block-library/src/button/edit.js
@@ -2,6 +2,10 @@
* External dependencies
*/
import classnames from 'classnames';
+/**
+ * WordPress dependencies
+ */
+import { store as bindingsStore } from '@wordpress/bindings';
/**
* Internal dependencies
@@ -45,7 +49,6 @@ import {
createBlock,
cloneBlock,
getDefaultBlockName,
- store as blocksStore,
} from '@wordpress/blocks';
import { useMergeRefs, useRefEffect } from '@wordpress/compose';
import { useSelect, useDispatch } from '@wordpress/data';
@@ -240,8 +243,8 @@ function ButtonEdit( props ) {
}
const blockBindingsSource = unlock(
- select( blocksStore )
- ).getBlockBindingsSource( metadata?.bindings?.url?.source );
+ select( bindingsStore )
+ ).getBindingsSource( metadata?.bindings?.url?.source );
return {
lockUrlControls:
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index fbbb2d481bee63..acdbc86820b193 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -7,7 +7,7 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
-import { store as blocksStore } from '@wordpress/blocks';
+import { store as bindingsStore } from '@wordpress/bindings';
import { Placeholder } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import {
@@ -343,8 +343,8 @@ export function ImageEdit( {
}
const blockBindingsSource = unlock(
- select( blocksStore )
- ).getBlockBindingsSource( metadata?.bindings?.url?.source );
+ select( bindingsStore )
+ ).getBindingsSource( metadata?.bindings?.url?.source );
return {
lockUrlControls:
@@ -362,6 +362,7 @@ export function ImageEdit( {
},
[ isSingleSelected ]
);
+
const placeholder = ( content ) => {
return (
0;
- const urlBindingSource = getBlockBindingsSource(
- urlBinding?.source
- );
- const altBindingSource = getBlockBindingsSource(
- altBinding?.source
- );
- const titleBindingSource = getBlockBindingsSource(
+ const urlBindingSource = getBindingsSource( urlBinding?.source );
+ const altBindingSource = getBindingsSource( altBinding?.source );
+ const titleBindingSource = getBindingsSource(
titleBinding?.source
);
return {
@@ -464,8 +461,8 @@ export default function Image( {
( ! urlBindingSource ||
urlBindingSource?.lockAttributesEditing ),
lockHrefControls:
- // Disable editing the link of the URL if the image is inside a pattern instance.
- // This is a temporary solution until we support overriding the link on the frontend.
+ // // Disable editing the link of the URL if the image is inside a pattern instance.
+ // // This is a temporary solution until we support overriding the link on the frontend.
hasParentPattern,
lockCaption:
// Disable editing the caption if the image is inside a pattern instance.
diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js
index d609f70b91b55d..ec89011b130cd9 100644
--- a/packages/blocks/src/store/private-actions.js
+++ b/packages/blocks/src/store/private-actions.js
@@ -51,7 +51,7 @@ export function registerBlockBindingsSource( source ) {
type: 'REGISTER_BLOCK_BINDINGS_SOURCE',
sourceName: source.name,
sourceLabel: source.label,
- useSource: source.useSource,
- lockAttributesEditing: source.lockAttributesEditing,
+ connect: source.connect,
+ lockAttributesEditing: false,
};
}
diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js
index f92fb376b530a7..5d771d7560a25c 100644
--- a/packages/blocks/src/store/reducer.js
+++ b/packages/blocks/src/store/reducer.js
@@ -389,8 +389,8 @@ export function blockBindingsSources( state = {}, action ) {
...state,
[ action.sourceName ]: {
label: action.sourceLabel,
- useSource: action.useSource,
- lockAttributesEditing: action.lockAttributesEditing ?? true,
+ connect: action.connect,
+ lockAttributesEditing: false,
},
};
}
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 507cc6b9d218ee..f4d442899ba12b 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -33,6 +33,7 @@
"@babel/runtime": "^7.16.0",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/bindings": "file:../bindings",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/blocks": "file:../blocks",
diff --git a/packages/editor/src/bindings/index.js b/packages/editor/src/bindings/index.js
index ce77b87ebc7a5c..bf13d0a139ab03 100644
--- a/packages/editor/src/bindings/index.js
+++ b/packages/editor/src/bindings/index.js
@@ -1,18 +1,28 @@
/**
* WordPress dependencies
*/
-import { store as blocksStore } from '@wordpress/blocks';
+import { store as bindingsStore } from '@wordpress/bindings';
import { dispatch } from '@wordpress/data';
+
/**
* Internal dependencies
*/
import { unlock } from '../lock-unlock';
import patternOverrides from './pattern-overrides';
import postMeta from './post-meta';
+import postEntity from './post-entity';
+
+const { registerBindingsSource } = unlock( dispatch( bindingsStore ) );
+
+// Lock attributes editing as default.
+postMeta.lockAttributesEditing =
+ typeof postMeta.lockAttributesEditing === 'undefined'
+ ? true
+ : postMeta.lockAttributesEditing;
-const { registerBlockBindingsSource } = unlock( dispatch( blocksStore ) );
-registerBlockBindingsSource( postMeta );
+registerBindingsSource( postMeta );
+registerBindingsSource( postEntity );
if ( process.env.IS_GUTENBERG_PLUGIN ) {
- registerBlockBindingsSource( patternOverrides );
+ registerBindingsSource( patternOverrides );
}
diff --git a/packages/editor/src/bindings/post-entity.js b/packages/editor/src/bindings/post-entity.js
new file mode 100644
index 00000000000000..96ff1db3323234
--- /dev/null
+++ b/packages/editor/src/bindings/post-entity.js
@@ -0,0 +1,46 @@
+/**
+ * WordPress dependencies
+ */
+import { store as coreStore } from '@wordpress/core-data';
+import { __ } from '@wordpress/i18n';
+import { select, dispatch } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import { store as editorStore } from '../store';
+import { RichTextData } from '@wordpress/rich-text';
+
+export default {
+ name: 'core/post-entity',
+ label: __( 'Post Entity' ),
+ connect( { prop, id } ) {
+ if ( ! prop ) {
+ throw new Error( 'The "prop" argument is required.' );
+ }
+
+ const { getEditedEntityRecord } = select( coreStore );
+ const { editEntityRecord } = dispatch( coreStore );
+ const { getCurrentPostId } = select( editorStore );
+
+ id = id || getCurrentPostId();
+
+ return {
+ get: () => {
+ const record = getEditedEntityRecord( 'postType', 'post', id );
+ return record[ prop ]?.rendered || record[ prop ];
+ },
+
+ update: ( newValue ) => {
+ if ( newValue instanceof RichTextData ) {
+ newValue = newValue.toString();
+ }
+
+ editEntityRecord( 'postType', 'post', id, {
+ [ prop ]: newValue,
+ } );
+ },
+ };
+ },
+
+ lockAttributesEditing: false,
+};
diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js
index 0d0c737d0eaf77..bd3d90ed01ad4f 100644
--- a/packages/editor/src/bindings/post-meta.js
+++ b/packages/editor/src/bindings/post-meta.js
@@ -1,8 +1,8 @@
/**
* WordPress dependencies
*/
-import { useEntityProp } from '@wordpress/core-data';
-import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { select, dispatch } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -12,33 +12,26 @@ import { store as editorStore } from '../store';
export default {
name: 'core/post-meta',
label: _x( 'Post Meta', 'block bindings source' ),
- useSource( props, sourceAttributes ) {
- const { getCurrentPostType } = useSelect( editorStore );
- const { context } = props;
- const { key: metaKey } = sourceAttributes;
- const postType = context.postType
- ? context.postType
- : getCurrentPostType();
+ connect( { key, id } ) {
+ const { getEditedEntityRecord } = select( coreStore );
+ const { editEntityRecord } = dispatch( coreStore );
+ const { getCurrentPostId } = select( editorStore );
- const [ meta, setMeta ] = useEntityProp(
- 'postType',
- context.postType,
- 'meta',
- context.postId
- );
-
- if ( postType === 'wp_template' ) {
- return { placeholder: metaKey };
- }
- const metaValue = meta[ metaKey ];
- const updateMetaValue = ( newValue ) => {
- setMeta( { ...meta, [ metaKey ]: newValue } );
- };
+ id = id || getCurrentPostId();
return {
- placeholder: metaKey,
- value: metaValue,
- updateValue: updateMetaValue,
+ get: () =>
+ getEditedEntityRecord( 'postType', 'post', id ).meta[ key ],
+
+ update: ( value ) => {
+ editEntityRecord( 'postType', 'post', id, {
+ meta: {
+ ...getEditedEntityRecord( 'postType', 'post', id ).meta,
+ [ key ]: value,
+ },
+ } );
+ },
};
},
+ lockAttributesEditing: false,
};
diff --git a/packages/editor/src/bindings/utils.js b/packages/editor/src/bindings/utils.js
new file mode 100644
index 00000000000000..f3cf8fa1dde06b
--- /dev/null
+++ b/packages/editor/src/bindings/utils.js
@@ -0,0 +1,32 @@
+const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
+ 'core/paragraph': [ 'content' ],
+ 'core/heading': [ 'content' ],
+ 'core/image': [ 'url', 'title', 'alt' ],
+ 'core/button': [ 'url', 'text', 'linkTarget' ],
+};
+
+/**
+ * Based on the given block name,
+ * check if it is possible to bind the block.
+ *
+ * @param {string} blockName - The block name.
+ * @return {boolean} Whether it is possible to bind the block to sources.
+ */
+export function canBindBlock( blockName ) {
+ return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
+}
+
+/**
+ * Based on the given block name and attribute name,
+ * check if it is possible to bind the block attribute.
+ *
+ * @param {string} blockName - The block name.
+ * @param {string} attributeName - The attribute name.
+ * @return {boolean} Whether it is possible to bind the block attribute.
+ */
+export function canBindAttribute( blockName, attributeName ) {
+ return (
+ canBindBlock( blockName ) &&
+ BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
+ );
+}