Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(POC) Block Bindings: introduce a second state to store incoming changes #59562

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 120 additions & 35 deletions packages/block-editor/src/hooks/use-bindings-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,46 +66,29 @@ export function canBindAttribute( blockName, attributeName ) {
* @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.
* @param {string} props.incomingAttrValue - The incoming attribute value.
* @return {null} Data-handling component. Render nothing.
*/
const BindingConnector = ( {
args,
attrName,
incomingAttrValue,
blockProps,
source,
onPropValueChange,
} ) => {
const { placeholder, value: propValue } = source.useSource(
blockProps,
args
);
const { useSource } = source;
const {
placeholder,
value: propValue,
updateValue: updatePropValue,
} = 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;
}
Expand All @@ -115,6 +98,11 @@ const BindingConnector = ( {
[ attrName, onPropValueChange ]
);

/*
* Source prop => Block Attribute
* Sync from source to the block attribute.
* It also handles the placeholder fallback.
*/
useLayoutEffect( () => {
if ( typeof propValue !== 'undefined' ) {
updateBoundAttibute( propValue, attrValue );
Expand Down Expand Up @@ -145,6 +133,22 @@ const BindingConnector = ( {
attrName,
] );

/*
* Block Attribute => Source prop
* Sync from incomming attribute value to the source.
*/
useLayoutEffect( () => {
if ( incomingAttrValue === undefined ) {
return;
}

if ( incomingAttrValue === propValue ) {
return;
}

updatePropValue( incomingAttrValue );
}, [ incomingAttrValue, propValue, updatePropValue ] );

return null;
};

Expand All @@ -154,13 +158,19 @@ const BindingConnector = ( {
* 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.
* @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.
* @param {Object} props.incomingBoundAttributes - The incoming bound attributes.
* @return {null} Data-handling component. Render nothing.
*/
function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) {
function BlockBindingBridge( {
blockProps,
bindings,
onPropValueChange,
incomingBoundAttributes,
} ) {
const blockBindingsSources = unlock(
useSelect( blocksStore )
).getAllBlockBindingsSources();
Expand All @@ -180,6 +190,9 @@ function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) {
<BindingConnector
key={ attrName }
attrName={ attrName }
incomingAttrValue={
incomingBoundAttributes[ attrName ]
}
source={ source }
blockProps={ blockProps }
args={ boundAttribute.args }
Expand All @@ -197,15 +210,40 @@ const withBlockBindingSupport = createHigherOrderComponent(
/*
* Collect and update the bound attributes
* in a separate state.
* Also, it checks the attribute type.
*/
const [ boundAttributes, setBoundAttributes ] = useState( {} );
const updateBoundAttributes = useCallback(
( newAttributes ) =>
setBoundAttributes( ( prev ) => ( {
( newAttributes ) => {
const nextAttributes = Object.fromEntries(
Object.entries( newAttributes ).map(
( [ attrName, attrValue ] ) => {
/*
* If the original attribute is a RichTextData instance,
* (core/paragraph, core/heading, core/button, etc.),
* convert the new value to a RichTextData instance, too.
*
* To do: it looks like a workaround.
* Consider improving the attribute and metadata fields types.
*/
const originalAttr = props.attributes[ attrName ];
if ( originalAttr instanceof RichTextData ) {
return [
attrName,
RichTextData.fromHTMLString( attrValue ),
];
}
return [ attrName, attrValue ];
}
)
);

return setBoundAttributes( ( prev ) => ( {
...prev,
...newAttributes,
} ) ),
[]
...nextAttributes,
} ) );
},
[ props.attributes ]
);

/*
Expand All @@ -218,19 +256,66 @@ const withBlockBindingSupport = createHigherOrderComponent(
)
);

const { setAttributes } = props;
const [ incomingBoundAttributes, setIncomingBoundAttributes ] =
useState( {} );

/**
* Helper function to update the block attributes.
*
* For unbound attributes, it calls the BlockEdit `setAttributes` callback.
* For bound attributes, it updates the incoming bound attributes,
* in a separate state.
*
* @param {Object} nextAttributes - The next attributes to update.
* @return {void}
*/
const updateAttributes = useCallback(
( nextAttributes ) => {
// Collect the unbound and bound attributes.
const unboundAttributes = {};
const nextBoundAttributes = {};
Object.entries( nextAttributes ).reduce(
( acc, [ key, value ] ) => {
if ( ! ( key in bindings ) ) {
acc.unbound[ key ] = value;
} else {
acc.bound[ key ] = value;
}
return acc;
},
{ unbound: unboundAttributes, bound: nextBoundAttributes }
);

// Update the unbound attributes only if there are any.
if ( Object.keys( unboundAttributes ).length ) {
setAttributes( unboundAttributes );
}

// Update the bound attributes.
setIncomingBoundAttributes( ( prev ) => ( {
...prev,
...nextBoundAttributes,
} ) );
},
[ bindings, setAttributes ]
);

return (
<>
{ Object.keys( bindings ).length > 0 && (
<BlockBindingBridge
blockProps={ props }
bindings={ bindings }
onPropValueChange={ updateBoundAttributes }
incomingBoundAttributes={ incomingBoundAttributes }
/>
) }

<BlockEdit
{ ...props }
attributes={ { ...props.attributes, ...boundAttributes } }
setAttributes={ updateAttributes }
/>
</>
);
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/bindings/post-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export default {
updateValue: updateMetaValue,
};
},
lockAttributesEditing: false,
};
Loading