Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Bindings: Unify getValue/getValues and setValue/setValues APIs #63185

Merged
merged 13 commits into from
Jul 19, 2024
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,
} );
SantosGuillamot marked this conversation as resolved.
Show resolved Hide resolved
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
Loading