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

Extensibility: Make Block Bindings work with editor.BlockEdit hook #67370

Merged
merged 7 commits into from
Dec 2, 2024
Merged
12 changes: 10 additions & 2 deletions packages/block-editor/src/components/block-edit/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { useContext, useMemo } from '@wordpress/element';
* Internal dependencies
*/
import BlockContext from '../block-context';
import { withBlockBindingsSupport } from './with-block-bindings-support';
import { canBindBlock } from '../../utils/block-bindings';

/**
* Default value used for blocks which do not define their own context needs,
Expand Down Expand Up @@ -47,6 +49,8 @@ const Edit = ( props ) => {

const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit );

const EditWithFiltersAndBindings = withBlockBindingsSupport( EditWithFilters );

const EditWithGeneratedProps = ( props ) => {
const { attributes = {}, name } = props;
const blockType = getBlockType( name );
Expand All @@ -67,8 +71,12 @@ const EditWithGeneratedProps = ( props ) => {
return null;
}

const EditComponent = canBindBlock( name )
? EditWithFiltersAndBindings
: EditWithFilters;

if ( blockType.apiVersion > 1 ) {
return <EditWithFilters { ...props } context={ context } />;
return <EditComponent { ...props } context={ context } />;
}

// Generate a class name for the block's editable form.
Expand All @@ -82,7 +90,7 @@ const EditWithGeneratedProps = ( props ) => {
);

return (
<EditWithFilters
<EditComponent
{ ...props }
context={ context }
className={ className }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,19 @@ import { store as blocksStore } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useRegistry, useSelect } from '@wordpress/data';
import { useCallback, useMemo, useContext } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';

/**
* Internal dependencies
*/
import isURLLike from '../components/link-control/is-url-like';
import { unlock } from '../lock-unlock';
import BlockContext from '../components/block-context';
import isURLLike from '../link-control/is-url-like';
import { unlock } from '../../lock-unlock';
import BlockContext from '../block-context';
import {
BLOCK_BINDINGS_ALLOWED_BLOCKS,
canBindAttribute,
} from '../../utils/block-bindings';

/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */
/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */

/**
* Given a binding of block attributes, returns a higher order component that
* overrides its `attributes` and `setAttributes` props to sync any changes needed.
*
* @return {WPHigherOrderComponent} Higher-order component.
*/

const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
'core/paragraph': [ 'content' ],
'core/heading': [ 'content' ],
'core/image': [ 'id', 'url', 'title', 'alt' ],
'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
};

const DEFAULT_ATTRIBUTE = '__default';

Expand Down Expand Up @@ -67,36 +55,12 @@ function replacePatternOverrideDefaultBindings( blockName, bindings ) {
}

/**
* Based on the given block name,
* check if it is possible to bind the block.
*
* @param {string} blockName - The block name.
* @return {boolean} Whether it is possible to bind the block to sources.
*/
export function canBindBlock( blockName ) {
return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
}

/**
* Based on the given block name and attribute name,
* check if it is possible to bind the block attribute.
* Given a binding of block attributes, returns a higher order component that
* overrides its `attributes` and `setAttributes` props to sync any changes needed.
*
* @param {string} blockName - The block name.
* @param {string} attributeName - The attribute name.
* @return {boolean} Whether it is possible to bind the block attribute.
* @return {WPHigherOrderComponent} Higher-order component.
*/
export function canBindAttribute( blockName, attributeName ) {
return (
canBindBlock( blockName ) &&
BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
);
}

export function getBindableAttributes( blockName ) {
return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
}

export const withBlockBindingSupport = createHigherOrderComponent(
export const withBlockBindingsSupport = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const registry = useRegistry();
const blockContext = useContext( BlockContext );
Expand All @@ -108,9 +72,9 @@ export const withBlockBindingSupport = createHigherOrderComponent(
() =>
replacePatternOverrideDefaultBindings(
name,
props.attributes.metadata?.bindings
props.attributes?.metadata?.bindings
gziolo marked this conversation as resolved.
Show resolved Hide resolved
),
[ props.attributes.metadata?.bindings, name ]
[ props.attributes?.metadata?.bindings, name ]
);

// While this hook doesn't directly call any selectors, `useSelect` is
Expand Down Expand Up @@ -196,7 +160,7 @@ export const withBlockBindingSupport = createHigherOrderComponent(

const hasParentPattern = !! updatedContext[ 'pattern/overrides' ];
const hasPatternOverridesDefaultBinding =
props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ]
props.attributes?.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ]
?.source === 'core/pattern-overrides';

const _setAttributes = useCallback(
Expand Down Expand Up @@ -283,40 +247,13 @@ export const withBlockBindingSupport = createHigherOrderComponent(
);

return (
<>
<BlockEdit
{ ...props }
attributes={ { ...props.attributes, ...boundAttributes } }
setAttributes={ _setAttributes }
context={ { ...context, ...updatedContext } }
/>
</>
<BlockEdit
{ ...props }
attributes={ { ...props.attributes, ...boundAttributes } }
setAttributes={ _setAttributes }
context={ { ...context, ...updatedContext } }
gziolo marked this conversation as resolved.
Show resolved Hide resolved
/>
);
},
'withBlockBindingSupport'
);

/**
* Filters a registered block's settings to enhance a block's `edit` component
* to upgrade bound attributes.
*
* @param {WPBlockSettings} settings - Registered block settings.
* @param {string} name - Block name.
* @return {WPBlockSettings} Filtered block settings.
*/
function shimAttributeSource( settings, name ) {
if ( ! canBindBlock( name ) ) {
return settings;
}

return {
...settings,
edit: withBlockBindingSupport( settings.edit ),
};
}

addFilter(
'blocks.registerBlockType',
'core/editor/custom-sources-backwards-compatibility/shim-attribute-source',
shimAttributeSource
);
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useBlockRefProvider } from './use-block-refs';
import { useIntersectionObserver } from './use-intersection-observer';
import { useScrollIntoView } from './use-scroll-into-view';
import { useFlashEditableBlocks } from '../../use-flash-editable-blocks';
import { canBindBlock } from '../../../hooks/use-bindings-attributes';
import { canBindBlock } from '../../../utils/block-bindings';
import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import FormatEdit from './format-edit';
import { getAllowedFormats } from './utils';
import { Content, valueToHTMLString } from './content';
import { withDeprecations } from './with-deprecations';
import { canBindBlock } from '../../hooks/use-bindings-attributes';
import { canBindBlock } from '../../utils/block-bindings';
import BlockContext from '../block-context';

export const keyboardShortcutContext = createContext();
Expand Down
10 changes: 5 additions & 5 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ import { useViewportMatch } from '@wordpress/compose';
/**
* Internal dependencies
*/
import {
canBindAttribute,
getBindableAttributes,
} from '../hooks/use-bindings-attributes';
import { unlock } from '../lock-unlock';
import InspectorControls from '../components/inspector-controls';
import BlockContext from '../components/block-context';
import { useBlockEditContext } from '../components/block-edit';
import { useBlockBindingsUtils } from '../utils/block-bindings';
import {
canBindAttribute,
getBindableAttributes,
useBlockBindingsUtils,
} from '../utils/block-bindings';
import { store as blockEditorStore } from '../store';

const { Menu } = unlock( componentsPrivateApis );
Expand Down
1 change: 0 additions & 1 deletion packages/block-editor/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import './metadata';
import blockHooks from './block-hooks';
import blockBindingsPanel from './block-bindings';
import './block-renaming';
import './use-bindings-attributes';
import './grid-visualizer';

createBlockEditFilter(
Expand Down
37 changes: 37 additions & 0 deletions packages/block-editor/src/utils/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,43 @@ function isObjectEmpty( object ) {
return ! object || Object.keys( object ).length === 0;
}

export const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
gziolo marked this conversation as resolved.
Show resolved Hide resolved
'core/paragraph': [ 'content' ],
'core/heading': [ 'content' ],
'core/image': [ 'id', 'url', 'title', 'alt' ],
'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
};

/**
* Based on the given block name,
* check if it is possible to bind the block.
*
* @param {string} blockName - The block name.
* @return {boolean} Whether it is possible to bind the block to sources.
*/
export function canBindBlock( blockName ) {
return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS;
}

/**
* Based on the given block name and attribute name,
* check if it is possible to bind the block attribute.
*
* @param {string} blockName - The block name.
* @param {string} attributeName - The attribute name.
* @return {boolean} Whether it is possible to bind the block attribute.
*/
export function canBindAttribute( blockName, attributeName ) {
return (
canBindBlock( blockName ) &&
BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName )
);
}

export function getBindableAttributes( blockName ) {
return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
}

/**
* Contains utils to update the block `bindings` metadata.
*
Expand Down
6 changes: 5 additions & 1 deletion packages/e2e-tests/plugins/block-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ function gutenberg_test_block_bindings_registration() {
plugins_url( 'block-bindings/index.js', __FILE__ ),
array(
'wp-blocks',
'wp-private-apis',
'wp-block-editor',
'wp-components',
'wp-compose',
'wp-element',
'wp-hooks',
),
filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ),
true
Expand Down
45 changes: 45 additions & 0 deletions packages/e2e-tests/plugins/block-bindings/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const { registerBlockBindingsSource } = wp.blocks;
const { InspectorControls } = wp.blockEditor;
const { PanelBody, TextControl } = wp.components;
const { createHigherOrderComponent } = wp.compose;
const { createElement: el, Fragment } = wp.element;
const { addFilter } = wp.hooks;
const { fieldsList } = window.testingBindings || {};

const getValues = ( { bindings } ) => {
Expand Down Expand Up @@ -46,3 +51,43 @@ registerBlockBindingsSource( {
getValues,
canUserEditValue: () => true,
} );

const withBlockBindingsInspectorControl = createHigherOrderComponent(
( BlockEdit ) => {
return ( props ) => {
if ( ! props.attributes?.metadata?.bindings?.content ) {
return el( BlockEdit, props );
}
Comment on lines +58 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid affecting other tests where content is connected, maybe it is safer to provide a unique name for this specific test? For example, checking props.attributes?.metadata?.bindings === 'bindings-inspector-controls'.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you explain how this generic UI control could affect other e2e tests and what the idea is to prevent it? bindings is an object so I'm not sure how to compare that with a string.

Copy link
Contributor

@SantosGuillamot SantosGuillamot Dec 2, 2024

Choose a reason for hiding this comment

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

Sorry, I meant metadata?.name, not metadata?.bindings.

The idea behind it was that right now it is adding the extra control for all the tests with content bound. I guess it shouldn't affect, but I thought it could be safer to provide a unique property (for example, a name to the block) from tests that need the extra control and check against that instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

What are other conditions that would get closer to real-life usage? I don't think extenders would ever target their integrations based on the name provided by the user through UI for the block name.

Copy link
Contributor

Choose a reason for hiding this comment

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

That'd be specific to the test. But it's okay, we can keep the control even for those tests where it is not needed.


return el(
Fragment,
{},
el( BlockEdit, props ),
el(
InspectorControls,
{},
el(
PanelBody,
{ title: 'Bindings' },
el( TextControl, {
__next40pxDefaultSize: true,
__nextHasNoMarginBottom: true,
label: 'Content',
value: props.attributes.content,
onChange: ( content ) =>
props.setAttributes( {
content,
} ),
} )
)
)
);
};
}
);

addFilter(
'editor.BlockEdit',
'testing/bindings-inspector-control',
withBlockBindingsInspectorControl
);
41 changes: 41 additions & 0 deletions test/e2e/specs/editor/various/block-bindings/post-meta.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,47 @@ test.describe( 'Post Meta source', () => {
previewPage.locator( '#connected-paragraph' )
).toHaveText( 'new value' );
} );

test( 'should be possible to edit the value of the connected custom fields in the inspector control registered by the plugin', async ( {
editor,
page,
} ) => {
await editor.insertBlock( {
name: 'core/paragraph',
attributes: {
anchor: 'connected-paragraph',
content: 'fallback content',
metadata: {
bindings: {
content: {
source: 'core/post-meta',
args: {
key: 'movie_field',
},
},
},
},
},
} );
const contentInput = page.getByRole( 'textbox', {
name: 'Content',
} );
await expect( contentInput ).toHaveValue(
'Movie field default value'
);
await contentInput.fill( 'new value' );
// Check that the paragraph content attribute didn't change.
const [ paragraphBlockObject ] = await editor.getBlocks();
expect( paragraphBlockObject.attributes.content ).toBe(
'fallback content'
);
// Check the value of the custom field is being updated by visiting the frontend.
const previewPage = await editor.openPreviewPage();
await expect(
previewPage.locator( '#connected-paragraph' )
).toHaveText( 'new value' );
} );

test( 'should be possible to connect movie fields through the attributes panel', async ( {
editor,
page,
Expand Down
Loading