diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index a783135c793e3f..2f7717091ff506 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -13,7 +13,7 @@ * @param WP_Block_Type $block_type Block Type. */ function gutenberg_register_pattern_support( $block_type ) { - $pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalConnections' ), false ) : false; + $pattern_support = 'core/paragraph' === $block_type->name ? true : false; if ( $pattern_support ) { if ( ! $block_type->uses_context ) { diff --git a/lib/experimental/block-bindings/html-processing.php b/lib/experimental/block-bindings/html-processing.php new file mode 100644 index 00000000000000..515749d0a8e753 --- /dev/null +++ b/lib/experimental/block-bindings/html-processing.php @@ -0,0 +1,110 @@ +get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $block_name ) { + $button_wrapper = $block_reader->get_tag(); + $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $button_wrapper_attrs = array(); + foreach ( $button_wrapper_attribute_names as $name ) { + $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $block_reader->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $selector_attrs = array(); + foreach ( $selector_attribute_names as $name ) { + $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + $selector_markup = "<$selector>" . esc_html( $source_value ) . ""; + $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); + $amended_content->next_tag(); + foreach ( $selector_attrs as $attribute_key => $attribute_value ) { + $amended_content->set_attribute( $attribute_key, $attribute_value ); + } + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { + return $amended_content->get_updated_html(); + } + if ( 'core/button' === $block_name ) { + $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; + $amended_button = new WP_HTML_Tag_Processor( $button_markup ); + $amended_button->next_tag(); + foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { + $amended_button->set_attribute( $attribute_key, $attribute_value ); + } + return $amended_button->get_updated_html(); + } + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + return $amended_content->get_updated_html(); + break; + + default: + return $block_content; + break; + } + return; + } +} diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php new file mode 100644 index 00000000000000..cca857e93702f3 --- /dev/null +++ b/lib/experimental/block-bindings/index.php @@ -0,0 +1,20 @@ + $label, + 'apply' => $apply, + ); + } +} diff --git a/lib/experimental/block-bindings/sources/pattern.php b/lib/experimental/block-bindings/sources/pattern.php new file mode 100644 index 00000000000000..e3456aa468d3eb --- /dev/null +++ b/lib/experimental/block-bindings/sources/pattern.php @@ -0,0 +1,21 @@ +attributes, array( 'metadata', 'id' ), false ) ) { + return null; + } + $block_id = $block_instance->attributes['metadata']['id']; + return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null ); + }; + register_block_bindings_source( + 'pattern_attributes', + __( 'Pattern Attributes', 'gutenberg' ), + $pattern_source_callback + ); +} diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/experimental/block-bindings/sources/post-meta.php new file mode 100644 index 00000000000000..99b6afc03c0d42 --- /dev/null +++ b/lib/experimental/block-bindings/sources/post-meta.php @@ -0,0 +1,25 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + + return get_post_meta( $post_id, $source_attrs['value'], true ); + }; + register_block_bindings_source( + 'post_meta', + __( 'Post Meta', 'gutenberg' ), + $post_meta_source_callback + ); +} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index d4bb6c9b4586eb..42663e127870c9 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -83,118 +83,90 @@ function wp_enqueue_block_view_script( $block_name, $args ) { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && ( - array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) || + array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) || array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) ) { - /** - * Renders the block meta attributes. - * - * @param string $block_content Block Content. - * @param array $block Block attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_render_block_connections( $block_content, $block, $block_instance ) { - $connection_sources = require __DIR__ . '/connection-sources/index.php'; - $block_type = $block_instance->block_type; - - // Allowlist of blocks that support block connections. - // Currently, we only allow the following blocks and attributes: - // - Paragraph: content. - // - Image: url. - $blocks_attributes_allowlist = array( - 'core/paragraph' => array( 'content' ), - 'core/image' => array( 'url' ), - ); - - // Whitelist of the block types that support block connections. - // Currently, we only allow the Paragraph and Image blocks to use block connections. - if ( ! in_array( $block['blockName'], array_keys( $blocks_attributes_allowlist ), true ) ) { - return $block_content; - } - - // If for some reason, the block type is not found, skip it. - if ( null === $block_type ) { - return $block_content; - } - - // If the block does not have support for block connections, skip it. - if ( ! block_has_support( $block_type, array( '__experimentalConnections' ), false ) ) { - return $block_content; - } - - // Get all the attributes that have a connection. - $connected_attributes = $block['attrs']['connections']['attributes'] ?? false; - if ( ! $connected_attributes ) { - return $block_content; - } - - foreach ( $connected_attributes as $attribute_name => $attribute_value ) { - - // If the attribute is not in the allowlist, skip it. - if ( ! in_array( $attribute_name, $blocks_attributes_allowlist[ $block['blockName'] ], true ) ) { - continue; - } - - // Skip if the source value is not "meta_fields" or "pattern_attributes". - if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) { - continue; - } - // If the attribute does not have a source, skip it. - if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { - continue; + require_once __DIR__ . '/block-bindings/index.php'; + // Allowed blocks that support block bindings. + // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? + global $block_bindings_allowed_blocks; + $block_bindings_allowed_blocks = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text' ), + ); + if ( ! function_exists( 'process_block_bindings' ) ) { + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function process_block_bindings( $block_content, $block, $block_instance ) { + // If the block doesn't have the bindings property, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) { + return $block_content; } - if ( 'pattern_attributes' === $attribute_value['source'] ) { - if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) { + // Assuming the following format for the bindings property of the "metadata" attribute: + // + // "bindings": { + // "title": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // }, + // "url": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // } + // } + // + global $block_bindings_allowed_blocks; + global $block_bindings_sources; + $modified_block_content = $block_content; + foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { + // If the block is not in the list, stop processing. + if ( ! isset( $block_bindings_allowed_blocks[ $block['blockName'] ] ) ) { + return $block_content; + } + // If the attribute is not in the list, process next attribute. + if ( ! in_array( $binding_attribute, $block_bindings_allowed_blocks[ $block['blockName'] ], true ) ) { continue; } - - $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance, $attribute_name ); - } else { - // If the attribute does not specify the name of the custom field, skip it. - if ( ! isset( $attribute_value['value'] ) ) { + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { continue; } - // Get the content from the connection source. - $custom_value = $connection_sources[ $attribute_value['source'] ]( - $block_instance, - $attribute_value['value'] - ); - } - - if ( false === $custom_value ) { - continue; - } + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; + // Get the value based on the source. + if ( ! isset( $binding_source['source']['attributes'] ) ) { + $source_args = array(); + } else { + $source_args = $binding_source['source']['attributes']; + } + $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); + // If the value is null, process next attribute. + if ( is_null( $source_value ) ) { + continue; + } - $tags = new WP_HTML_Tag_Processor( $block_content ); - $found = $tags->next_tag( - array( - // TODO: In the future, when blocks other than Paragraph and Image are - // supported, we should build the full query from CSS selector. - 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], - ) - ); - if ( ! $found ) { - return $block_content; + // Process the HTML based on the block and the attribute. + $modified_block_content = block_bindings_replace_html( $modified_block_content, $block['blockName'], $binding_attribute, $source_value ); } - $tag_name = $tags->get_tag(); - $markup = "<$tag_name>$custom_value"; - $updated_tags = new WP_HTML_Tag_Processor( $markup ); - $updated_tags->next_tag(); - - // Get all the attributes from the original block and add them to the new markup. - $names = $tags->get_attribute_names_with_prefix( '' ); - foreach ( $names as $name ) { - $updated_tags->set_attribute( $name, $tags->get_attribute( $name ) ); - } - - return $updated_tags->get_updated_html(); + return $modified_block_content; } - return $block_content; + // Add filter only to the blocks in the list. + foreach ( $block_bindings_allowed_blocks as $block_name => $attributes ) { + add_filter( 'render_block_' . $block_name, 'process_block_bindings', 20, 3 ); + } } - - add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 ); } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php deleted file mode 100644 index 4f9e06cb13b945..00000000000000 --- a/lib/experimental/connection-sources/index.php +++ /dev/null @@ -1,23 +0,0 @@ - 'meta', - 'meta_fields' => function ( $block_instance, $meta_field ) { - // We should probably also check if the meta field exists but for now it's okay because - // if it doesn't, `get_post_meta()` will just return an empty string. - return get_post_meta( $block_instance->context['postId'], $meta_field, true ); - }, - 'pattern_attributes' => function ( $block_instance, $attribute_name ) { - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( - $block_instance->context, - array( 'pattern/overrides', $block_id, $attribute_name ), - false - ); - }, -); diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 5f61684e8b1342..729376cf030dd9 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -26,8 +26,8 @@ function gutenberg_enable_experiments() { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalConnections = true', 'before' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindings = true', 'before' ); } if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 2d2e76273d2d59..8af1eb82c6bed0 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -128,13 +128,13 @@ function gutenberg_initialize_experiments_settings() { add_settings_field( 'gutenberg-custom-fields', - __( 'Connections', 'gutenberg' ), + __( 'Block Bindings & Custom Fields', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test connecting block attribute values to a custom field value', 'gutenberg' ), - 'id' => 'gutenberg-connections', + 'label' => __( 'Test connecting block attributes to different sources like custom fields', 'gutenberg' ), + 'id' => 'gutenberg-block-bindings', ) ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js deleted file mode 100644 index 9b677933adc138..00000000000000 --- a/packages/block-editor/src/hooks/custom-fields.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * WordPress dependencies - */ -import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { hasBlockSupport } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { InspectorControls } from '../components'; -import { useBlockEditingMode } from '../components/block-editing-mode'; - -/** - * Filters registered block settings, extending attributes to include `connections`. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addAttribute( settings ) { - if ( hasBlockSupport( settings, '__experimentalConnections', true ) ) { - // Gracefully handle if settings.attributes.connections is undefined. - settings.attributes = { - ...settings.attributes, - connections: { - type: 'object', - }, - }; - } - - return settings; -} - -function CustomFieldsControlPure( { name, connections, setAttributes } ) { - const blockEditingMode = useBlockEditingMode(); - if ( blockEditingMode !== 'default' ) { - return null; - } - - // If the block is a paragraph or image block, we need to know which - // attribute to use for the connection. Only the `content` attribute - // of the paragraph block and the `url` attribute of the image block are supported. - let attributeName; - if ( name === 'core/paragraph' ) attributeName = 'content'; - if ( name === 'core/image' ) attributeName = 'url'; - - return ( - - - { - if ( nextValue === '' ) { - setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } - } } - /> - - - ); -} - -export default { - edit: CustomFieldsControlPure, - attributeKeys: [ 'connections' ], - hasSupport( name ) { - return ( - hasBlockSupport( name, '__experimentalConnections', false ) && - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - [ 'core/paragraph', 'core/image' ].includes( name ) - ); - }, -}; - -if ( - window.__experimentalConnections || - window.__experimentalPatternPartialSyncing -) { - addFilter( - 'blocks.registerBlockType', - 'core/editor/connections/attribute', - addAttribute - ); -} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 385b9fe6b1511e..f17c0a22166e4e 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -25,7 +25,6 @@ import layout from './layout'; import childLayout from './layout-child'; import contentLockUI from './content-lock-ui'; import './metadata'; -import customFields from './custom-fields'; import blockHooks from './block-hooks'; import blockRenaming from './block-renaming'; @@ -39,7 +38,6 @@ createBlockEditFilter( position, layout, contentLockUI, - window.__experimentalConnections ? customFields : null, blockHooks, blockRenaming, ].filter( Boolean ) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 57db2d166f9f99..6331d33c27a7bb 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -27,7 +27,7 @@ import { store as blockEditorStore, BlockControls, } from '@wordpress/block-editor'; -import { getBlockSupport, parse, cloneBlock } from '@wordpress/blocks'; +import { parse, cloneBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -38,17 +38,17 @@ const { useLayoutClasses } = unlock( blockEditorPrivateApis ); function isPartiallySynced( block ) { return ( - !! getBlockSupport( block.name, '__experimentalConnections', false ) && - !! block.attributes.connections?.attributes && - Object.values( block.attributes.connections.attributes ).some( - ( connection ) => connection.source === 'pattern_attributes' + 'core/paragraph' === block.name && + !! block.attributes.metadata?.bindings && + Object.values( block.attributes.metadata.bindings ).some( + ( binding ) => binding.source.name === 'pattern_attributes' ) ); } function getPartiallySyncedAttributes( block ) { - return Object.entries( block.attributes.connections.attributes ) + return Object.entries( block.attributes.metadata.bindings ) .filter( - ( [ , connection ] ) => connection.source === 'pattern_attributes' + ( [ , binding ] ) => binding.source.name === 'pattern_attributes' ) .map( ( [ attributeKey ] ) => attributeKey ); } diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 3fe4fbb34e1029..a81d754d8ca1be 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -41,7 +41,6 @@ "text": true } }, - "__experimentalConnections": true, "spacing": { "margin": true, "padding": true, diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js index 40bd1e16dfc00d..a940890dfa693a 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useBlockEditingMode } from '@wordpress/block-editor'; -import { hasBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; /** @@ -32,11 +31,6 @@ const { const withPartialSyncingControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const blockEditingMode = useBlockEditingMode(); - const hasCustomFieldsSupport = hasBlockSupport( - props.name, - '__experimentalConnections', - false - ); const isEditingPattern = useSelect( ( select ) => select( editorStore ).getCurrentPostType() === @@ -45,7 +39,6 @@ const withPartialSyncingControls = createHigherOrderComponent( ); const shouldShowPartialSyncingControls = - hasCustomFieldsSupport && props.isSelected && isEditingPattern && blockEditingMode === 'default' && diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js index d20bd1d347012e..f5ac19bc05f3d7 100644 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -19,7 +19,7 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; const attributeSources = Object.keys( syncedAttributes ).map( ( attributeName ) => - attributes.connections?.attributes?.[ attributeName ]?.source + attributes.metadata?.bindings?.[ attributeName ]?.source?.name ); const isConnectedToOtherSources = attributeSources.every( ( source ) => source && source !== 'pattern_attributes' @@ -30,52 +30,58 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { return null; } - function updateConnections( isChecked ) { - let updatedConnections = { - ...attributes.connections, - attributes: { ...attributes.connections?.attributes }, + function updateBindings( isChecked ) { + let updatedBindings = { + ...attributes?.metadata?.bindings, }; if ( ! isChecked ) { for ( const attributeName of Object.keys( syncedAttributes ) ) { if ( - updatedConnections.attributes[ attributeName ]?.source === + updatedBindings[ attributeName ]?.source?.name === 'pattern_attributes' ) { - delete updatedConnections.attributes[ attributeName ]; + delete updatedBindings[ attributeName ]; } } - if ( ! Object.keys( updatedConnections.attributes ).length ) { - delete updatedConnections.attributes; - } - if ( ! Object.keys( updatedConnections ).length ) { - updatedConnections = undefined; + if ( ! Object.keys( updatedBindings ).length ) { + updatedBindings = undefined; } setAttributes( { - connections: updatedConnections, + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, } ); return; } for ( const attributeName of Object.keys( syncedAttributes ) ) { - if ( ! updatedConnections.attributes[ attributeName ] ) { - updatedConnections.attributes[ attributeName ] = { - source: 'pattern_attributes', + if ( ! updatedBindings[ attributeName ] ) { + updatedBindings[ attributeName ] = { + source: { + name: 'pattern_attributes', + }, }; } } if ( typeof attributes.metadata?.id === 'string' ) { - setAttributes( { connections: updatedConnections } ); + setAttributes( { + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, + } ); return; } const id = nanoid( 6 ); setAttributes( { - connections: updatedConnections, metadata: { ...attributes.metadata, id, + bindings: updatedBindings, }, } ); } @@ -93,7 +99,7 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { ( source ) => source === 'pattern_attributes' ) } onChange={ ( isChecked ) => { - updateConnections( isChecked ); + updateBindings( isChecked ); } } />