Skip to content

Commit

Permalink
Block bindings API: don't use hooks (#60724)
Browse files Browse the repository at this point in the history
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: SantosGuillamot <[email protected]>
  • Loading branch information
4 people authored Apr 17, 2024
1 parent f5ee54f commit d4ba8f4
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 190 deletions.
251 changes: 90 additions & 161 deletions packages/block-editor/src/hooks/use-bindings-attributes.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 (
<BindingConnector
key={ attrName }
attrName={ attrName }
source={ source }
blockProps={ blockProps }
args={ boundAttribute.args }
onPropValueChange={ onPropValueChange }
/>
);
}
) }
</>
);
}

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 && (
<BlockBindingBridge
blockProps={ props }
bindings={ bindings }
onPropValueChange={ updateBoundAttributes }
/>
) }

<BlockEdit
{ ...props }
attributes={ { ...props.attributes, ...boundAttributes } }
setAttributes={ _setAttributes }
/>
</>
);
Expand Down
4 changes: 3 additions & 1 deletion packages/blocks/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
4 changes: 3 additions & 1 deletion packages/blocks/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down
38 changes: 11 additions & 27 deletions packages/editor/src/bindings/post-meta.js
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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 ];
},
};

0 comments on commit d4ba8f4

Please sign in to comment.