Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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