Skip to content

Commit

Permalink
Extensibility: Make Block Bindings work with editor.BlockEdit hook (#…
Browse files Browse the repository at this point in the history
…67370)

* Block Bindings: Move the place when the attributes get overriden

* Fix failing unit tests

* Wrap Edit with bindings logic only when the block supports it

* Extend the test plugin with `editor.BlockEdit` filter

* Add a new test covering the extensibility inside inspector controls

* Fix the issue with missing context established by sources

Co-authored-by: gziolo <[email protected]>
Co-authored-by: SantosGuillamot <[email protected]>
  • Loading branch information
3 people authored and michalczaplinski committed Dec 5, 2024
1 parent cdd8dc0 commit d0da94d
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 94 deletions.
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
),
[ 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 } }
/>
);
},
'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 = {
'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 );
}

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

0 comments on commit d0da94d

Please sign in to comment.