From 6fa790c885f0b468e0b9df038c5e05fccafe648a Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 13 Apr 2024 20:56:31 +0300 Subject: [PATCH] Block bindings: don't use hooks --- .../src/hooks/use-bindings-attributes.js | 280 ++++++++---------- packages/blocks/src/store/private-actions.js | 4 +- packages/blocks/src/store/reducer.js | 3 + packages/editor/src/bindings/post-meta.js | 38 +-- 4 files changed, 133 insertions(+), 192 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 5cd8cb46b3b7e7..6ae1c606577f8f 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,134 @@ 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 ) { +export const withBlockBindingSupport = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const registry = useRegistry(); + + const boundAttributes = useSelect( + ( select ) => { + const bindings = Object.fromEntries( + Object.entries( + props.attributes.metadata?.bindings || {} + ).filter( ( [ attrName ] ) => + canBindAttribute( props.name, attrName ) + ) + ); + + if ( ! Object.keys( bindings ).length > 0 ) { 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; -}; + const blockBindingsSources = unlock( + select( blocksStore ) + ).getAllBlockBindingsSources(); + + return Object.entries( bindings ).reduce( + ( accu, [ attrName, boundAttribute ] ) => { + // Bail early if the block doesn't have a valid source handler. + const source = + blockBindingsSources[ boundAttribute.source ]; + + if ( ! source?.getValue ) { + return accu; + } + + const args = { + registry, + context: props.context, + clientId: props.clientId, + attributeName: attrName, + args: boundAttribute.args, + }; + + accu[ attrName ] = source.getValue( args ); + + if ( accu[ attrName ] === undefined ) { + if ( attrName === 'url' ) { + accu[ attrName ] = null; + } else { + accu[ attrName ] = + source.getPlaceholder?.( args ); + } + } + + return accu; + }, + {} + ); + }, + [ + props.attributes.metadata?.bindings, + props.name, + props.context, + props.clientId, + registry, + ] + ); -/** - * 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; + + const _setAttributes = useCallback( + ( nextAttributes ) => { + const keptAttributes = { ...nextAttributes }; + registry.batch( () => { + const bindings = Object.fromEntries( + Object.entries( + props.attributes.metadata?.bindings || {} + ).filter( ( [ attrName ] ) => + canBindAttribute( props.name, attrName ) + ) + ); - 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; + if ( ! Object.keys( bindings ).length > 0 ) { + 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 blockBindingsSources = unlock( + registry.select( blocksStore ) + ).getAllBlockBindingsSources(); + + for ( const [ attributeKey, value ] of Object.entries( + nextAttributes + ) ) { + if ( bindings[ attributeKey ] ) { + const source = + blockBindingsSources[ + bindings[ attributeKey ].source + ]; + if ( source?.setValue ) { + source.setValue( { + registry, + context: props.context, + clientId: props.clientId, + attributeName: attributeKey, + value, + args: bindings[ attributeKey ].args, + } ); + delete keptAttributes[ attributeKey ]; + } + } + } - /* - * 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 ) - ) + setAttributes( keptAttributes ); + } ); + }, + [ + registry, + props.attributes.metadata?.bindings, + props.name, + props.context, + props.clientId, + setAttributes, + ] ); 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 d609f70b91b55d..1ef9c3614922e0 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 f92fb376b530a7..7a7dac93b3fb77 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -390,6 +390,9 @@ export function blockBindingsSources( state = {}, action ) { [ 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 0d0c737d0eaf77..f5b3b526dbfd4a 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 ]; }, };