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

Block Bindings: Create utils to update or remove bindings #64102

Merged
merged 6 commits into from
Aug 21, 2024
Merged
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
111 changes: 27 additions & 84 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
__experimentalVStack as VStack,
privateApis as componentsPrivateApis,
} from '@wordpress/components';
import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
import { useRegistry } from '@wordpress/data';
import { useContext, Fragment } from '@wordpress/element';
import { useViewportMatch } from '@wordpress/compose';

Expand All @@ -24,10 +24,10 @@ import {
canBindAttribute,
getBindableAttributes,
} from '../hooks/use-bindings-attributes';
import { store as blockEditorStore } from '../store';
import { unlock } from '../lock-unlock';
import InspectorControls from '../components/inspector-controls';
import BlockContext from '../components/block-context';
import { useBlockBindingsUtils } from '../utils/block-bindings';

const {
DropdownMenuV2: DropdownMenu,
Expand All @@ -51,17 +51,15 @@ const useToolsPanelDropdownMenuProps = () => {
: {};
};

function BlockBindingsPanelDropdown( {
fieldsList,
addConnection,
attribute,
binding,
} ) {
function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) {
const { getBlockBindingsSources } = unlock( blocksPrivateApis );
const registeredSources = getBlockBindingsSources();
const { updateBlockBindings } = useBlockBindingsUtils();
const currentKey = binding?.args?.key;
return (
<>
{ Object.entries( fieldsList ).map( ( [ label, fields ], i ) => (
<Fragment key={ label }>
{ Object.entries( fieldsList ).map( ( [ name, fields ], i ) => (
<Fragment key={ name }>
<DropdownMenuGroup>
{ Object.keys( fieldsList ).length > 1 && (
<Text
Expand All @@ -70,14 +68,19 @@ function BlockBindingsPanelDropdown( {
variant="muted"
aria-hidden
>
{ label }
{ registeredSources[ name ].label }
</Text>
) }
{ Object.entries( fields ).map( ( [ key, value ] ) => (
<DropdownMenuRadioItem
key={ key }
onChange={ () =>
addConnection( key, attribute )
updateBlockBindings( {
[ attribute ]: {
source: name,
args: { key },
},
} )
}
name={ attribute + '-binding' }
value={ key }
Expand Down Expand Up @@ -141,9 +144,8 @@ function EditableBlockBindingsPanelItems( {
attributes,
bindings,
fieldsList,
addConnection,
removeConnection,
} ) {
const { updateBlockBindings } = useBlockBindingsUtils();
const isMobile = useViewportMatch( 'medium', '<' );
return (
<>
Expand All @@ -155,7 +157,9 @@ function EditableBlockBindingsPanelItems( {
hasValue={ () => !! binding }
label={ attribute }
onDeselect={ () => {
removeConnection( attribute );
updateBlockBindings( {
[ attribute ]: undefined,
} );
} }
>
<DropdownMenu
Expand All @@ -175,7 +179,6 @@ function EditableBlockBindingsPanelItems( {
>
<BlockBindingsPanelDropdown
fieldsList={ fieldsList }
addConnection={ addConnection }
attribute={ attribute }
binding={ binding }
/>
Expand All @@ -187,91 +190,33 @@ function EditableBlockBindingsPanelItems( {
);
}

export const BlockBindingsPanel = ( { name, metadata } ) => {
export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
const registry = useRegistry();
const blockContext = useContext( BlockContext );
const { bindings } = metadata || {};

const bindableAttributes = getBindableAttributes( name );
const { removeAllBlockBindings } = useBlockBindingsUtils();
const bindableAttributes = getBindableAttributes( blockName );
const dropdownMenuProps = useToolsPanelDropdownMenuProps();

const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
if (
! canBindAttribute( name, key ) ||
! canBindAttribute( blockName, key ) ||
filteredBindings[ key ].source === 'core/pattern-overrides'
) {
delete filteredBindings[ key ];
}
} );

const { updateBlockAttributes } = useDispatch( blockEditorStore );

const { _id } = useSelect( ( select ) => {
const { getSelectedBlockClientId } = select( blockEditorStore );

return {
_id: getSelectedBlockClientId(),
};
}, [] );

if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return null;
}

const removeAllConnections = () => {
const newMetadata = { ...metadata };
delete newMetadata.bindings;
updateBlockAttributes( _id, {
metadata:
Object.keys( newMetadata ).length === 0
? undefined
: newMetadata,
} );
};

const addConnection = ( value, attribute ) => {
// Assuming the block expects a flat structure for its metadata attribute
const newMetadata = {
...metadata,
// Adjust this according to the actual structure expected by your block
bindings: {
...metadata?.bindings,
[ attribute ]: {
source: 'core/post-meta',
args: { key: value },
},
},
};
// Update the block's attributes with the new metadata
updateBlockAttributes( _id, {
metadata: newMetadata,
} );
};

const removeConnection = ( key ) => {
const newMetadata = { ...metadata };
if ( ! newMetadata.bindings ) {
return;
}

delete newMetadata.bindings[ key ];
if ( Object.keys( newMetadata.bindings ).length === 0 ) {
delete newMetadata.bindings;
}
updateBlockAttributes( _id, {
metadata:
Object.keys( newMetadata ).length === 0
? undefined
: newMetadata,
} );
};

const fieldsList = {};
const { getBlockBindingsSources } = unlock( blocksPrivateApis );
const registeredSources = getBlockBindingsSources();
Object.values( registeredSources ).forEach(
( { getFieldsList, label, usesContext } ) => {
Object.entries( registeredSources ).forEach(
( [ sourceName, { getFieldsList, usesContext } ] ) => {
if ( getFieldsList ) {
// Populate context.
const context = {};
Expand All @@ -286,7 +231,7 @@ export const BlockBindingsPanel = ( { name, metadata } ) => {
} );
// Only add source if the list is not empty.
if ( sourceList ) {
fieldsList[ label ] = { ...sourceList };
fieldsList[ sourceName ] = { ...sourceList };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were we using the label here before? It seems like that was brittle. It appears that this change is unrelated to the adding of the utils in this PR, and is just an additional code cleanup, and it looks good. I'm just curious.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we were just using the values of the registeredSources object instead of their keys.

I'm not sure if if sources label were always the same as the registeredSources key, but this approach seems safer, as the label should allow whitespaces or uppercases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were we using the label here before?

I believe the name wasn't exposed before. But this seemed safer to me as well.

}
}
}
Expand All @@ -312,7 +257,7 @@ export const BlockBindingsPanel = ( { name, metadata } ) => {
<ToolsPanel
label={ __( 'Attributes' ) }
resetAll={ () => {
removeAllConnections();
removeAllBlockBindings();
} }
dropdownMenuProps={ dropdownMenuProps }
className="block-editor-bindings__panel"
Expand All @@ -327,8 +272,6 @@ export const BlockBindingsPanel = ( { name, metadata } ) => {
attributes={ bindableAttributes }
bindings={ filteredBindings }
fieldsList={ fieldsList }
addConnection={ addConnection }
removeConnection={ removeConnection }
/>
) }
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { PrivatePublishDateTimePicker } from './components/publish-date-time-pic
import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes';
import useBlockDisplayTitle from './components/block-title/use-block-display-title';
import TabbedSidebar from './components/tabbed-sidebar';
import { useBlockBindingsUtils } from './utils/block-bindings';

/**
* Private @wordpress/block-editor APIs.
Expand Down Expand Up @@ -93,4 +94,5 @@ lock( privateApis, {
useBlockDisplayTitle,
__unstableBlockStyleVariationOverridesWithConfig,
setBackgroundStyleDefaults,
useBlockBindingsUtils,
} );
98 changes: 98 additions & 0 deletions packages/block-editor/src/utils/block-bindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../store';
import { useBlockEditContext } from '../components/block-edit';

export function useBlockBindingsUtils() {
const { clientId } = useBlockEditContext();
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const { getBlockAttributes } = useSelect( blockEditorStore );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more optimal to have:

const { getBlockAttributes } = useRegistry().select( blockEditorStore );

See more in this guide.


/**
* Updates the value of the bindings connected to block attributes.
* It removes the binding when the new value is `undefined`.
*
* @param {Object} bindings Bindings including the attributes to update and the new object.
* @param {string} bindings.source The source name to connect to.
* @param {Object} [bindings.args] Object containing the arguments needed by the source.
*
* @example
* ```js
* import { useBlockBindingsUtils } from '@wordpress/block-editor'
*
* const { updateBlockBindings } = useBlockBindingsUtils();
* updateBlockBindings( {
* url: {
* source: 'core/post-meta',
* args: {
* key: 'url_custom_field',
* },
* },
* alt: {
* source: 'core/post-meta',
* args: {
* key: 'text_custom_field',
* },
* }
* } );
* ```
*/
const updateBlockBindings = ( bindings ) => {
const { metadata } = getBlockAttributes( clientId );
const newBindings = { ...metadata?.bindings };
Object.entries( bindings ).forEach( ( [ attribute, binding ] ) => {
if ( ! binding && newBindings[ attribute ] ) {
delete newBindings[ attribute ];
return;
}
newBindings[ attribute ] = binding;
} );

const newMetadata = {
...metadata,
bindings: newBindings,
};

if ( Object.keys( newMetadata.bindings ).length === 0 ) {
Copy link
Member

@gziolo gziolo Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in other places it would increase the readability if the helper function was present:

function isObjectEmpty( object ) {
	return ! object || Object.keys( object ).length === 0;
}

// later
if ( isObjectEmpty( newMetadata.bindings ) ) {

This could also be further refactored so it doesn't require the step to remove the undefined key:

const newMetadata = {
	...metadata,
	...( ! isObjectEmpty( newBindings ) && {
		bindings: newBindings,
	} ),
};

delete newMetadata.bindings;
}

updateBlockAttributes( clientId, {
metadata:
Object.keys( newMetadata ).length === 0
? undefined
: newMetadata,
} );
};

/**
* Removes the bindings property of the `metadata` attribute.
*
* @example
* ```js
* import { useBlockBindingsUtils } from '@wordpress/block-editor'
*
* const { removeAllBlockBindings } = useBlockBindingsUtils();
* removeAllBlockBindings();
* ```
*/
const removeAllBlockBindings = () => {
const { metadata } = getBlockAttributes( clientId );
const newMetadata = { ...metadata };
delete newMetadata.bindings;
Comment on lines +86 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a single line:

const { metadata: { bindings, ...metadata } } = getBlockAttributes( clientId );

updateBlockAttributes( clientId, {
metadata:
Object.keys( newMetadata ).length === 0
? undefined
: newMetadata,
} );
};

return { updateBlockBindings, removeAllBlockBindings };
}
Loading
Loading