diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php
index cec1eafcf94fa..3233624557a5c 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 d10be06feef19..74b2ad01672f7 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 0000000000000..dbc8c3ec2c089
--- /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 a66aa0a73ed41..6834d859d2545 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 7e13b13dc4feb..db30d95db8475 100644
--- a/packages/block-library/src/paragraph/block.json
+++ b/packages/block-library/src/paragraph/block.json
@@ -41,6 +41,7 @@
"text": true
}
},
+ "__experimentalConnections": true,
"spacing": {
"margin": true,
"padding": true,