diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php
index fafeba00e3b3f6..febb404394a4f9 100644
--- a/lib/experimental/blocks.php
+++ b/lib/experimental/blocks.php
@@ -121,3 +121,107 @@ function gutenberg_register_metadata_attribute( $args ) {
return $args;
}
add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' );
+
+
+$gutenberg_experiments = get_option( 'gutenberg-experiments' );
+if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $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 = _wp_array_get( $block['attrs'], array( '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;
+ }
+
+ // If the source value is not "meta_fields", skip it because the only supported
+ // connection source is meta (custom fields) for now.
+ if ( 'meta_fields' !== $attribute_value['source'] ) {
+ continue;
+ }
+
+ // If the attribute does not have a source, skip it.
+ if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
+ continue;
+ }
+
+ // If the attribute does not specify the name of the custom field, skip it.
+ if ( ! isset( $attribute_value['value'] ) ) {
+ continue;
+ }
+
+ // Get the content from the connection source.
+ $custom_value = $connection_sources[ $attribute_value['source'] ](
+ $block_instance,
+ $attribute_value['value']
+ );
+
+ $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;
+ };
+ $tag_name = $tags->get_tag();
+ $markup = "<$tag_name>$custom_value$tag_name>";
+ $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 $block_content;
+ }
+ 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
new file mode 100644
index 00000000000000..b63abcad96f628
--- /dev/null
+++ b/lib/experimental/connection-sources/index.php
@@ -0,0 +1,15 @@
+ '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 );
+ },
+);
diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index cec1eafcf94fa0..3233624557a5cb 100644
--- a/lib/experimental/editor-settings.php
+++ b/lib/experimental/editor-settings.php
@@ -90,6 +90,10 @@ 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_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
}
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index d10be06feef19a..74b2ad01672f71 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -115,6 +115,18 @@ function gutenberg_initialize_experiments_settings() {
)
);
+ add_settings_field(
+ 'gutenberg-custom-fields',
+ __( 'Connections', 'gutenberg' ),
+ 'gutenberg_display_experiment_field',
+ 'gutenberg-experiments',
+ 'gutenberg_experiments_section',
+ array(
+ 'label' => __( 'Test Connections', 'gutenberg' ),
+ 'id' => 'gutenberg-connections',
+ )
+ );
+
register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js
new file mode 100644
index 00000000000000..dbc8c3ec2c089f
--- /dev/null
+++ b/packages/block-editor/src/hooks/custom-fields.js
@@ -0,0 +1,139 @@
+/**
+ * WordPress dependencies
+ */
+import { addFilter } from '@wordpress/hooks';
+import { PanelBody, TextControl } from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+import { hasBlockSupport } from '@wordpress/blocks';
+import { createHigherOrderComponent } from '@wordpress/compose';
+
+/**
+ * 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;
+}
+
+/**
+ * Override the default edit UI to include a new block inspector control for
+ * assigning a connection to blocks that has support for connections.
+ * Currently, only the `core/paragraph` block is supported and there is only a relation
+ * between paragraph content and a custom field.
+ *
+ * @param {WPComponent} BlockEdit Original component.
+ *
+ * @return {WPComponent} Wrapped component.
+ */
+const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {
+ return ( props ) => {
+ const blockEditingMode = useBlockEditingMode();
+ const hasCustomFieldsSupport = hasBlockSupport(
+ props.name,
+ '__experimentalConnections',
+ false
+ );
+
+ // Check if the current block is a paragraph or image block.
+ // Currently, only these two blocks are supported.
+ if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) {
+ return ;
+ }
+
+ // 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 ( props.name === 'core/paragraph' ) attributeName = 'content';
+ if ( props.name === 'core/image' ) attributeName = 'url';
+
+ if ( hasCustomFieldsSupport && props.isSelected ) {
+ return (
+ <>
+
+ { blockEditingMode === 'default' && (
+
+
+ {
+ if ( nextValue === '' ) {
+ props.setAttributes( {
+ connections: undefined,
+ [ attributeName ]: undefined,
+ placeholder: undefined,
+ } );
+ } else {
+ props.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
+ ),
+ } );
+ }
+ } }
+ />
+
+
+ ) }
+ >
+ );
+ }
+
+ return ;
+ };
+}, 'withInspectorControl' );
+
+if ( window.__experimentalConnections ) {
+ addFilter(
+ 'blocks.registerBlockType',
+ 'core/connections/attribute',
+ addAttribute
+ );
+ addFilter(
+ 'editor.BlockEdit',
+ 'core/connections/with-inspector-control',
+ withInspectorControl
+ );
+}
diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js
index a66aa0a73ed411..6834d859d25453 100644
--- a/packages/block-editor/src/hooks/index.js
+++ b/packages/block-editor/src/hooks/index.js
@@ -21,6 +21,7 @@ import './content-lock-ui';
import './metadata';
import './metadata-name';
import './behaviors';
+import './custom-fields';
export { useCustomSides } from './dimensions';
export { useLayoutClasses, useLayoutStyles } from './layout';
diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json
index 7e13b13dc4feb9..85f56f4a838f50 100644
--- a/packages/block-library/src/paragraph/block.json
+++ b/packages/block-library/src/paragraph/block.json
@@ -7,6 +7,7 @@
"description": "Start with the basic building block of all narrative.",
"keywords": [ "text" ],
"textdomain": "default",
+ "usesContext": [ "postId" ],
"attributes": {
"align": {
"type": "string"
@@ -41,6 +42,7 @@
"text": true
}
},
+ "__experimentalConnections": true,
"spacing": {
"margin": true,
"padding": true,