Skip to content

Commit

Permalink
Block Bindings: Unify getValue/getValues and setValue/`setValue…
Browse files Browse the repository at this point in the history
…s` APIs (WordPress#63185)

* Rename to `setValuesInBatch`

* Pass whole binding instead of just the new value

* Use `setValuesInBatch` in post meta

* Add support for `getValuesInBatch`

* Use `getValuesInBatch` in post meta

* Use `getValuesInBatch` in pattern overrides

* Use only `getValues` and `setValues`

* Fix pattern overrides

* Check undefined instead of !

* Fix error after rebasing

* Change registration checks

* Use `bindings` param for the API

* Use `blockBindingsBySource` name

Co-authored-by: SantosGuillamot <santosguillamot@git.wordpress.org>
Co-authored-by: artemiomorales <artemiosans@git.wordpress.org>
Co-authored-by: cbravobernal <cbravobernal@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>
  • Loading branch information
5 people committed Jul 19, 2024
1 parent a0fb3c6 commit 43569d2
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 119 deletions.
124 changes: 67 additions & 57 deletions packages/block-editor/src/hooks/use-bindings-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
} );
}
}

Expand All @@ -242,7 +252,7 @@ export const withBlockBindingSupport = createHigherOrderComponent(
},
[
registry,
bindings,
blockBindings,
name,
clientId,
context,
Expand Down
21 changes: 6 additions & 15 deletions packages/blocks/src/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -796,8 +794,7 @@ export const registerBlockBindingsSource = ( source ) => {
const {
name,
label,
getValue,
setValue,
getValues,
setValues,
getPlaceholder,
canUserEditValue,
Expand Down Expand Up @@ -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;
}

Expand Down
27 changes: 6 additions & 21 deletions packages/blocks/src/api/test/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
} );
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions packages/blocks/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions packages/blocks/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 28 additions & 15 deletions packages/editor/src/bindings/pattern-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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 ) => {
Expand Down
Loading

0 comments on commit 43569d2

Please sign in to comment.