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) (WordPress#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 and yogeshbhutkar committed Dec 18, 2024
1 parent 04537eb commit 86a03b8
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

0 comments on commit 86a03b8

Please sign in to comment.