diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 5cd8cb46b3b7e..5ca526b5749b7 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -1,12 +1,11 @@ /** * WordPress dependencies */ -import { getBlockType, store as blocksStore } from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; -import { useLayoutEffect, useCallback, useState } from '@wordpress/element'; +import { useRegistry, useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; -import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -56,181 +55,111 @@ export function canBindAttribute( blockName, 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 ) { +export const withBlockBindingSupport = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const registry = useRegistry(); + const sources = useSelect( ( select ) => + unlock( select( blocksStore ) ).getAllBlockBindingsSources() + ); + const bindings = props.attributes.metadata?.bindings; + const { name, clientId, context } = props; + const boundAttributes = useSelect( () => { + if ( ! bindings ) { 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; + const attributes = {}; + + for ( const [ attributeName, boundAttribute ] of Object.entries( + bindings + ) ) { + const source = sources[ boundAttribute.source ]; + if ( + ! source?.getValue || + ! canBindAttribute( name, attributeName ) + ) { + continue; + } - if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) { - updateBoundAttibute( null ); - return; + const args = { + registry, + context, + clientId, + attributeName, + args: boundAttribute.args, + }; + + attributes[ attributeName ] = source.getValue( args ); + + if ( attributes[ attributeName ] === undefined ) { + if ( attributeName === 'url' ) { + attributes[ attributeName ] = null; + } else { + attributes[ attributeName ] = + source.getPlaceholder?.( args ); + } + } } - updateBoundAttibute( placeholder ); - } - }, [ - updateBoundAttibute, - propValue, - attrValue, - placeholder, - blockName, - attrName, - ] ); - - return null; -}; + return attributes; + }, [ bindings, name, clientId, context, registry, sources ] ); -/** - * 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(); + const { setAttributes } = props; - 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; + const _setAttributes = useCallback( + ( nextAttributes ) => { + registry.batch( () => { + if ( ! bindings ) { + return setAttributes( nextAttributes ); } - 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, - } ) ), - [] - ); + const keptAttributes = { ...nextAttributes }; + + for ( const [ + attributeName, + boundAttribute, + ] of Object.entries( bindings ) ) { + const source = sources[ boundAttribute.source ]; + if ( + ! source?.setValue || + ! canBindAttribute( name, attributeName ) + ) { + continue; + } + + source.setValue( { + registry, + context, + clientId, + attributeName, + args: boundAttribute.args, + value: nextAttributes[ attributeName ], + } ); + delete keptAttributes[ attributeName ]; + } - /* - * 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 ) - ) + if ( Object.keys( keptAttributes ).length ) { + setAttributes( keptAttributes ); + } + } ); + }, + [ + registry, + bindings, + name, + clientId, + context, + setAttributes, + sources, + ] ); return ( <> - { Object.keys( bindings ).length > 0 && ( - - ) } - ); diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index d609f70b91b55..1ef9c3614922e 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -51,7 +51,9 @@ export function registerBlockBindingsSource( source ) { type: 'REGISTER_BLOCK_BINDINGS_SOURCE', sourceName: source.name, sourceLabel: source.label, - useSource: source.useSource, + getValue: source.getValue, + setValue: source.setValue, + getPlaceholder: source.getPlaceholder, lockAttributesEditing: source.lockAttributesEditing, }; } diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index f92fb376b530a..39b5bb7c9a7ea 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -389,7 +389,9 @@ export function blockBindingsSources( state = {}, action ) { ...state, [ action.sourceName ]: { label: action.sourceLabel, - useSource: action.useSource, + getValue: action.getValue, + setValue: action.setValue, + getPlaceholder: action.getPlaceholder, lockAttributesEditing: action.lockAttributesEditing ?? true, }, }; diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 0d0c737d0eaf7..f5b3b526dbfd4 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -1,9 +1,9 @@ /** * WordPress dependencies */ -import { useEntityProp } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; import { _x } from '@wordpress/i18n'; + /** * Internal dependencies */ @@ -12,33 +12,17 @@ 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; + getPlaceholder( { args } ) { + return args.key; + }, + getValue( { registry, context, args } ) { const postType = context.postType ? context.postType - : getCurrentPostType(); - - 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 } ); - }; + : registry.select( editorStore ).getCurrentPostType(); - return { - placeholder: metaKey, - value: metaValue, - updateValue: updateMetaValue, - }; + return registry + .select( coreDataStore ) + .getEditedEntityRecord( 'postType', postType, context.postId ) + .meta?.[ args.key ]; }, };