Skip to content

Commit

Permalink
Block bindings: don't use hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Apr 13, 2024
1 parent e9bccc8 commit 6fa790c
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 192 deletions.
280 changes: 116 additions & 164 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,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 (
<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 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 && (
<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,
};
}
3 changes: 3 additions & 0 deletions packages/blocks/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
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 6fa790c

Please sign in to comment.