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
…(2nd try) (#67523)

* Block Bindings: Refactor `withBlockBindingSupport`

* Fix issues reported by CI

* Move bindings handling to EditWithGeneratedProps

* Move sources context to `useMemo`

* Reuse `getBlockBindingsSources`

* Add e2e test

* Add missing dependencies to block bindings JS script in e2e test

* Refactor block bindings utils

* Final touches on EditWithGeneratedProps

* Improve block bindings utils

* Address feedback from code review

---------

Co-authored-by: Mario Santos <[email protected]>

Co-authored-by: gziolo <[email protected]>
Co-authored-by: SantosGuillamot <[email protected]>
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
  • Loading branch information
5 people authored Dec 11, 2024
1 parent 7952846 commit 114aa68
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 339 deletions.
229 changes: 218 additions & 11 deletions packages/block-editor/src/components/block-edit/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@ import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { withFilters } from '@wordpress/components';
import {
getBlockDefaultClassName,
hasBlockSupport,
getBlockType,
hasBlockSupport,
store as blocksStore,
} from '@wordpress/blocks';
import { useContext, useMemo } from '@wordpress/element';
import { withFilters } from '@wordpress/components';
import { useRegistry, useSelect } from '@wordpress/data';
import { useCallback, useContext, useMemo } from '@wordpress/element';

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

/**
* Default value used for blocks which do not define their own context needs,
Expand Down Expand Up @@ -48,27 +57,223 @@ const Edit = ( props ) => {
const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit );

const EditWithGeneratedProps = ( props ) => {
const { attributes = {}, name } = props;
const { name, clientId, attributes, setAttributes } = props;
const registry = useRegistry();
const blockType = getBlockType( name );
const blockContext = useContext( BlockContext );
const registeredSources = useSelect(
( select ) =>
unlock( select( blocksStore ) ).getAllBlockBindingsSources(),
[]
);

// Assign context values using the block type's declared context needs.
const context = useMemo( () => {
return blockType && blockType.usesContext
const { blockBindings, context, hasPatternOverrides } = useMemo( () => {
// Assign context values using the block type's declared context needs.
const computedContext = blockType?.usesContext
? Object.fromEntries(
Object.entries( blockContext ).filter( ( [ key ] ) =>
blockType.usesContext.includes( key )
)
)
: DEFAULT_BLOCK_CONTEXT;
}, [ blockType, blockContext ] );
// Add context requested by Block Bindings sources.
if ( attributes?.metadata?.bindings ) {
Object.values( attributes?.metadata?.bindings || {} ).forEach(
( binding ) => {
registeredSources[ binding?.source ]?.usesContext?.forEach(
( key ) => {
computedContext[ key ] = blockContext[ key ];
}
);
}
);
}
return {
blockBindings: replacePatternOverridesDefaultBinding(
name,
attributes?.metadata?.bindings
),
context: computedContext,
hasPatternOverrides: hasPatternOverridesDefaultBinding(
attributes?.metadata?.bindings
),
};
}, [
name,
blockType?.usesContext,
blockContext,
attributes?.metadata?.bindings,
registeredSources,
] );

const computedAttributes = useSelect(
( select ) => {
if ( ! blockBindings ) {
return attributes;
}

const attributesFromSources = {};
const blockBindingsBySource = new Map();

for ( const [ attributeName, binding ] of Object.entries(
blockBindings
) ) {
const { source: sourceName, args: sourceArgs } = binding;
const source = registeredSources[ sourceName ];
if ( ! source || ! canBindAttribute( name, attributeName ) ) {
continue;
}

blockBindingsBySource.set( source, {
...blockBindingsBySource.get( source ),
[ attributeName ]: {
args: sourceArgs,
},
} );
}

if ( blockBindingsBySource.size ) {
for ( const [ source, bindings ] of blockBindingsBySource ) {
// Get values in batch if the source supports it.
let values = {};
if ( ! source.getValues ) {
Object.keys( bindings ).forEach( ( attr ) => {
// Default to the the source label when `getValues` doesn't exist.
values[ attr ] = source.label;
} );
} else {
values = source.getValues( {
select,
context,
clientId,
bindings,
} );
}
for ( const [ attributeName, value ] of Object.entries(
values
) ) {
if (
attributeName === 'url' &&
( ! value || ! isURLLike( value ) )
) {
// Return null if value is not a valid URL.
attributesFromSources[ attributeName ] = null;
} else {
attributesFromSources[ attributeName ] = value;
}
}
}
}

return {
...attributes,
...attributesFromSources,
};
},
[
attributes,
blockBindings,
clientId,
context,
name,
registeredSources,
]
);

const setBoundAttributes = useCallback(
( nextAttributes ) => {
if ( ! blockBindings ) {
setAttributes( nextAttributes );
return;
}

registry.batch( () => {
const keptAttributes = { ...nextAttributes };
const blockBindingsBySource = new Map();

// Loop only over the updated attributes to avoid modifying the bound ones that haven't changed.
for ( const [ attributeName, newValue ] of Object.entries(
keptAttributes
) ) {
if (
! blockBindings[ attributeName ] ||
! canBindAttribute( name, attributeName )
) {
continue;
}

const binding = blockBindings[ attributeName ];
const source = registeredSources[ binding?.source ];
if ( ! source?.setValues ) {
continue;
}
blockBindingsBySource.set( source, {
...blockBindingsBySource.get( source ),
[ attributeName ]: {
args: binding.args,
newValue,
},
} );
delete keptAttributes[ attributeName ];
}

if ( blockBindingsBySource.size ) {
for ( const [
source,
bindings,
] of blockBindingsBySource ) {
source.setValues( {
select: registry.select,
dispatch: registry.dispatch,
context,
clientId,
bindings,
} );
}
}

const hasParentPattern = !! context[ 'pattern/overrides' ];

if (
// Don't update non-connected attributes if the block is using pattern overrides
// and the editing is happening while overriding the pattern (not editing the original).
! ( hasPatternOverrides && hasParentPattern ) &&
Object.keys( keptAttributes ).length
) {
// Don't update caption and href until they are supported.
if ( hasPatternOverrides ) {
delete keptAttributes.caption;
delete keptAttributes.href;
}
setAttributes( keptAttributes );
}
} );
},
[
blockBindings,
clientId,
context,
hasPatternOverrides,
setAttributes,
registeredSources,
name,
registry,
]
);

if ( ! blockType ) {
return null;
}

if ( blockType.apiVersion > 1 ) {
return <EditWithFilters { ...props } context={ context } />;
return (
<EditWithFilters
{ ...props }
attributes={ computedAttributes }
context={ context }
setAttributes={ setBoundAttributes }
/>
);
}

// Generate a class name for the block's editable form.
Expand All @@ -77,15 +282,17 @@ const EditWithGeneratedProps = ( props ) => {
: null;
const className = clsx(
generatedClassName,
attributes.className,
attributes?.className,
props.className
);

return (
<EditWithFilters
{ ...props }
context={ context }
attributes={ computedAttributes }
className={ className }
context={ context }
setAttributes={ setBoundAttributes }
/>
);
};
Expand Down
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
4 changes: 2 additions & 2 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import { useViewportMatch } from '@wordpress/compose';
import {
canBindAttribute,
getBindableAttributes,
} from '../hooks/use-bindings-attributes';
useBlockBindingsUtils,
} from '../utils/block-bindings';
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 { 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
Loading

1 comment on commit 114aa68

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 114aa68.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12273408517
📝 Reported issues:

Please sign in to comment.