diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index a63f59b69ca21c..410ad93bd971a7 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -101,7 +101,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( const hasPatternOverridesDefaultBinding = props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] ?.source === 'core/pattern-overrides'; - const bindings = useMemo( + const blockBindings = useMemo( () => replacePatternOverrideDefaultBindings( name, @@ -115,110 +115,120 @@ export const withBlockBindingSupport = createHigherOrderComponent( // there are attribute updates. // `source.getValues` may also call a selector via `registry.select`. const boundAttributes = useSelect( () => { - if ( ! bindings ) { + if ( ! blockBindings ) { return; } const attributes = {}; - for ( const [ attributeName, boundAttribute ] of Object.entries( - bindings + const blockBindingsBySource = new Map(); + + for ( const [ attributeName, binding ] of Object.entries( + blockBindings ) ) { - const source = sources[ boundAttribute.source ]; + const { source: sourceName, args: sourceArgs } = binding; + const source = sources[ sourceName ]; if ( - ! source?.getValue || + ! source?.getValues || ! canBindAttribute( name, attributeName ) ) { continue; } - const args = { - registry, - context, - clientId, - attributeName, - args: boundAttribute.args, - }; - - attributes[ attributeName ] = source.getValue( args ); + blockBindingsBySource.set( source, { + ...blockBindingsBySource.get( source ), + [ attributeName ]: { + args: sourceArgs, + }, + } ); + } - if ( attributes[ attributeName ] === undefined ) { - if ( attributeName === 'url' ) { - attributes[ attributeName ] = null; - } else { - attributes[ attributeName ] = - source.getPlaceholder?.( args ); + if ( blockBindingsBySource.size ) { + for ( const [ source, bindings ] of blockBindingsBySource ) { + // Get values in batch if the source supports it. + const values = source.getValues( { + registry, + context, + clientId, + bindings, + } ); + for ( const [ attributeName, value ] of Object.entries( + values + ) ) { + // Use placeholder when value is undefined. + if ( value === undefined ) { + if ( attributeName === 'url' ) { + attributes[ attributeName ] = null; + } else { + attributes[ attributeName ] = + source.getPlaceholder?.( { + registry, + context, + clientId, + attributeName, + args: bindings[ attributeName ].args, + } ); + } + } else { + attributes[ attributeName ] = value; + } } } } return attributes; - }, [ bindings, name, clientId, context, registry, sources ] ); + }, [ blockBindings, name, clientId, context, registry, sources ] ); const { setAttributes } = props; const _setAttributes = useCallback( ( nextAttributes ) => { registry.batch( () => { - if ( ! bindings ) { + if ( ! blockBindings ) { setAttributes( nextAttributes ); return; } const keptAttributes = { ...nextAttributes }; - const updatesBySource = new Map(); + 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 ( - ! bindings[ attributeName ] || + ! blockBindings[ attributeName ] || ! canBindAttribute( name, attributeName ) ) { continue; } - const binding = bindings[ attributeName ]; + const binding = blockBindings[ attributeName ]; const source = sources[ binding?.source ]; - if ( ! source?.setValue && ! source?.setValues ) { + if ( ! source?.setValues ) { continue; } - updatesBySource.set( source, { - ...updatesBySource.get( source ), - [ attributeName ]: newValue, + blockBindingsBySource.set( source, { + ...blockBindingsBySource.get( source ), + [ attributeName ]: { + args: binding.args, + newValue, + }, } ); delete keptAttributes[ attributeName ]; } - if ( updatesBySource.size ) { + if ( blockBindingsBySource.size ) { for ( const [ source, - attributes, - ] of updatesBySource ) { - if ( source.setValues ) { - source.setValues( { - registry, - context, - clientId, - attributes, - } ); - } else { - for ( const [ - attributeName, - value, - ] of Object.entries( attributes ) ) { - const binding = bindings[ attributeName ]; - source.setValue( { - registry, - context, - clientId, - attributeName, - args: binding.args, - value, - } ); - } - } + bindings, + ] of blockBindingsBySource ) { + source.setValues( { + registry, + context, + clientId, + bindings, + } ); } } @@ -242,7 +252,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( }, [ registry, - bindings, + blockBindings, name, clientId, context, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 2dd433bca383e9..78aab99b11b617 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -770,8 +770,7 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * @param {Object} source Properties of the source to be registered. * @param {string} source.name The unique and machine-readable name. * @param {string} source.label Human-readable label. - * @param {Function} [source.getValue] Function to get the value of the source. - * @param {Function} [source.setValue] Function to update the value of the source. + * @param {Function} [source.getValues] Function to get the values from the source. * @param {Function} [source.setValues] Function to update multiple values connected to the source. * @param {Function} [source.getPlaceholder] Function to get the placeholder when the value is undefined. * @param {Function} [source.canUserEditValue] Function to determine if the user can edit the value. @@ -784,8 +783,7 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * registerBlockBindingsSource( { * name: 'plugin/my-custom-source', * label: _x( 'My Custom Source', 'block bindings source' ), - * getValue: () => 'Value to place in the block attribute', - * setValue: () => updateMyCustomValue(), + * getValues: () => getSourceValues(), * setValues: () => updateMyCustomValuesInBatch(), * getPlaceholder: () => 'Placeholder text when the value is undefined', * canUserEditValue: () => true, @@ -796,8 +794,7 @@ export const registerBlockBindingsSource = ( source ) => { const { name, label, - getValue, - setValue, + getValues, setValues, getPlaceholder, canUserEditValue, @@ -857,15 +854,9 @@ export const registerBlockBindingsSource = ( source ) => { return; } - // Check the `getValue` property is correct. - if ( getValue && typeof getValue !== 'function' ) { - warning( 'Block bindings source getValue must be a function.' ); - return; - } - - // Check the `setValue` property is correct. - if ( setValue && typeof setValue !== 'function' ) { - warning( 'Block bindings source setValue must be a function.' ); + // Check the `getValues` property is correct. + if ( getValues && typeof getValues !== 'function' ) { + warning( 'Block bindings source getValues must be a function.' ); return; } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 991b299c08abf6..bc1057597bcd7a 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1512,28 +1512,15 @@ describe( 'blocks', () => { expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined(); } ); - // Check the `getValue` callback is correct. - it( 'should reject invalid getValue callback', () => { + // Check the `getValues` callback is correct. + it( 'should reject invalid getValues callback', () => { registerBlockBindingsSource( { name: 'core/testing', label: 'testing', - getValue: 'should be a function', + getValues: 'should be a function', } ); expect( console ).toHaveWarnedWith( - 'Block bindings source getValue must be a function.' - ); - expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined(); - } ); - - // Check the `setValue` callback is correct. - it( 'should reject invalid setValue callback', () => { - registerBlockBindingsSource( { - name: 'core/testing', - label: 'testing', - setValue: 'should be a function', - } ); - expect( console ).toHaveWarnedWith( - 'Block bindings source setValue must be a function.' + 'Block bindings source getValues must be a function.' ); expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined(); } ); @@ -1581,8 +1568,7 @@ describe( 'blocks', () => { it( 'should register a valid source', () => { const sourceProperties = { label: 'Valid Source', - getValue: () => 'value', - setValue: () => 'new value', + getValues: () => 'value', setValues: () => 'new values', getPlaceholder: () => 'placeholder', canUserEditValue: () => true, @@ -1603,8 +1589,7 @@ describe( 'blocks', () => { label: 'Valid Source', } ); const source = getBlockBindingsSource( 'core/valid-source' ); - expect( source.getValue ).toBeUndefined(); - expect( source.setValue ).toBeUndefined(); + expect( source.getValues ).toBeUndefined(); expect( source.setValues ).toBeUndefined(); expect( source.getPlaceholder ).toBeUndefined(); expect( source.canUserEditValue ).toBeUndefined(); diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index a59ed157e98693..55cdb2128895f5 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -51,8 +51,7 @@ export function addBlockBindingsSource( source ) { type: 'ADD_BLOCK_BINDINGS_SOURCE', name: source.name, label: source.label, - getValue: source.getValue, - setValue: source.setValue, + getValues: source.getValues, setValues: source.setValues, getPlaceholder: source.getPlaceholder, canUserEditValue: source.canUserEditValue, diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 4237590f8887a4..fc386e7ea9f557 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -378,8 +378,7 @@ export function blockBindingsSources( state = {}, action ) { ...state, [ action.name ]: { label: action.label, - getValue: action.getValue, - setValue: action.setValue, + getValues: action.getValues, setValues: action.setValues, getPlaceholder: action.getPlaceholder, canUserEditValue: action.canUserEditValue, diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 54ca77650a5fe9..b299211900095d 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -9,28 +9,32 @@ const CONTENT = 'content'; export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), - getValue( { registry, clientId, context, attributeName } ) { + getValues( { registry, clientId, context, bindings } ) { const patternOverridesContent = context[ 'pattern/overrides' ]; const { getBlockAttributes } = registry.select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); - if ( ! patternOverridesContent ) { - return currentBlockAttributes[ attributeName ]; - } - - const overridableValue = - patternOverridesContent?.[ - currentBlockAttributes?.metadata?.name - ]?.[ attributeName ]; + const overridesValues = {}; + for ( const attributeName of Object.keys( bindings ) ) { + const overridableValue = + patternOverridesContent?.[ + currentBlockAttributes?.metadata?.name + ]?.[ attributeName ]; - // If there is no pattern client ID, or it is not overwritten, return the default value. - if ( overridableValue === undefined ) { - return currentBlockAttributes[ attributeName ]; + // If it has not been overriden, return the original value. + // Check undefined because empty string is a valid value. + if ( overridableValue === undefined ) { + overridesValues[ attributeName ] = + currentBlockAttributes[ attributeName ]; + continue; + } else { + overridesValues[ attributeName ] = + overridableValue === '' ? undefined : overridableValue; + } } - - return overridableValue === '' ? undefined : overridableValue; + return overridesValues; }, - setValues( { registry, clientId, attributes } ) { + setValues( { registry, clientId, bindings } ) { const { getBlockAttributes, getBlockParentsByBlockName, getBlocks } = registry.select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); @@ -45,6 +49,15 @@ export default { true ); + // Extract the updated attributes from the source bindings. + const attributes = Object.entries( bindings ).reduce( + ( attrs, [ key, { newValue } ] ) => { + attrs[ key ] = newValue; + return attrs; + }, + {} + ); + // If there is no pattern client ID, sync blocks with the same name and same attributes. if ( ! patternClientId ) { const syncBlocksWithSameName = ( blocks ) => { diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index a2fb5964663978..f8161dd47b5c41 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -15,22 +15,29 @@ export default { getPlaceholder( { args } ) { return args.key; }, - getValue( { registry, context, args } ) { - return registry + getValues( { registry, context, bindings } ) { + const meta = registry .select( coreDataStore ) .getEditedEntityRecord( 'postType', context?.postType, context?.postId - ).meta?.[ args.key ]; + )?.meta; + const newValues = {}; + for ( const [ attributeName, source ] of Object.entries( bindings ) ) { + newValues[ attributeName ] = meta?.[ source.args.key ]; + } + return newValues; }, - setValue( { registry, context, args, value } ) { + setValues( { registry, context, bindings } ) { + const newMeta = {}; + Object.values( bindings ).forEach( ( { args, newValue } ) => { + newMeta[ args.key ] = newValue; + } ); registry .dispatch( coreDataStore ) .editEntityRecord( 'postType', context?.postType, context?.postId, { - meta: { - [ args.key ]: value, - }, + meta: newMeta, } ); }, canUserEditValue( { select, context, args } ) {