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

POC: Implement partially synced patterns using the block bindings API #55807

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Create and save content to reuse across your site. Update the pattern, and the c
- **Name:** core/block
- **Category:** reusable
- **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~, ~~renaming~~
- **Attributes:** ref
- **Attributes:** dynamicContent, ref

## Button

Expand Down
31 changes: 31 additions & 0 deletions lib/block-supports/pattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* Pattern block support flag.
*
* @package gutenberg
*/

$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) {
function gutenberg_register_pattern_support( $block_type ) {
$pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalConnections' ), false ) : false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting, the injected context is only necessary when dealing with Partially Synced Patterns. The condition based on the UI elements seems to work fine here, but it isn't a direct relationship.


if ( $pattern_support ) {
if ( ! $block_type->uses_context ) {
$block_type->uses_context = array();
}

if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) {
$block_type->uses_context[] = 'dynamicContent';
}
}
}

// Register the block support.
WP_Block_Supports::get_instance()->register(
'pattern',
array(
'register_attribute' => 'gutenberg_register_pattern_support',
)
);
}
11 changes: 8 additions & 3 deletions lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
$blocks_attributes_allowlist = array(
'core/paragraph' => array( 'content' ),
'core/image' => array( 'url' ),
'core/heading' => array( 'content' ),
);

// Whitelist of the block types that support block connections.
Expand Down Expand Up @@ -132,9 +133,8 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
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'] ) {
// Skip if the source value is not "meta_fields" or "pattern_attributes".
if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) {
continue;
}

Expand All @@ -154,6 +154,10 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
$attribute_value['value']
);

if ( false === $custom_value ) {
continue;
}

$tags = new WP_HTML_Tag_Processor( $block_content );
$found = $tags->next_tag(
array(
Expand Down Expand Up @@ -181,5 +185,6 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst

return $block_content;
}

add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 );
}
3 changes: 3 additions & 0 deletions lib/experimental/connection-sources/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
// 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, $meta_field ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to get confirmation from @michalczaplinski or @SantosGuillamot on how this array should be shaped. There is name key defined with the meta value, which implies, we should have another entry with the name pattern. I might be completely wrong here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that nothing was ultimately decided here! Just for reference, this is a previous discussion with Riad: #53247 (comment)

We'll need a nested array here OR have a one file per source (like in Riad's original prototype) so meta.php, patterns.php, etc.

return _wp_array_get( $block_instance->context, array( 'dynamicContent', $meta_field ), false );
}
);
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ function () {
require __DIR__ . '/block-supports/shadow.php';
require __DIR__ . '/block-supports/background.php';
require __DIR__ . '/block-supports/behaviors.php';
require __DIR__ . '/block-supports/pattern.php';

// Data views.
require_once __DIR__ . '/experimental/data-views.php';
133 changes: 100 additions & 33 deletions packages/block-editor/src/hooks/custom-fields.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* WordPress dependencies
*/
import { useRegistry } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { PanelBody, TextControl } from '@wordpress/components';
import { PanelBody, TextControl, SelectControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { hasBlockSupport } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
Expand Down Expand Up @@ -55,7 +56,11 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {

// 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 ) ) {
if (
! [ 'core/paragraph', 'core/image', 'core/heading' ].includes(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed enabling the Button block, too. It's fine to take care of it as a follow-up. I'm leaving it mostly as a note.

props.name
)
) {
return <BlockEdit { ...props } />;
}

Expand All @@ -65,6 +70,41 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {
let attributeName;
if ( props.name === 'core/paragraph' ) attributeName = 'content';
if ( props.name === 'core/image' ) attributeName = 'url';
if ( props.name === 'core/heading' ) attributeName = 'content';

const connectionSource =
props.attributes?.connections?.attributes?.[ attributeName ]
?.source || '';
const connectionValue =
props.attributes?.connections?.attributes?.[ attributeName ]
?.value || '';

function updateConnections( source, value ) {
if ( value === '' ) {
props.setAttributes( {
connections: 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,
value,
},
},
},
placeholder: sprintf(
'This content will be replaced on the frontend by the value of "%s" custom field.',
value
),
} );
}
}

if ( hasCustomFieldsSupport && props.isSelected ) {
return (
Expand All @@ -76,42 +116,40 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {
title={ __( 'Connections' ) }
initialOpen={ true }
>
<SelectControl
label={ __( 'Source' ) }
value={ connectionSource }
options={ [
{
label: __( 'None' ),
value: '',
},
{
label: __( 'Meta fields' ),
value: 'meta_fields',
},
{
label: __( 'Pattern attributes' ),
value: 'pattern_attributes',
},
] }
onChange={ ( nextSource ) => {
updateConnections(
nextSource,
connectionValue
);
} }
/>
<TextControl
__nextHasNoMarginBottom
autoComplete="off"
label={ __( 'Custom field meta_key' ) }
value={
props.attributes?.connections
?.attributes?.[ attributeName ]
?.value || ''
}
value={ connectionValue }
onChange={ ( nextValue ) => {
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
),
} );
}
updateConnections(
connectionSource,
nextValue
);
} }
/>
</PanelBody>
Expand All @@ -125,6 +163,30 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => {
};
}, 'withInspectorControl' );

const createEditFunctionWithPatternSource = () =>
createHigherOrderComponent(
( BlockEdit ) =>
( { attributes, ...props } ) => {
const registry = useRegistry();
const sourceAttributes =
registry._selectAttributes?.( {
clientId: props.clientId,
name: props.name,
attributes,
} ) ?? attributes;

return (
<BlockEdit { ...props } attributes={ sourceAttributes } />
);
}
);

function shimAttributeSource( settings ) {
settings.edit = createEditFunctionWithPatternSource()( settings.edit );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the role of this HOC?

There is a sub-registry injected in the core/block edit implementation that should play a similar role. It essentially should allow redefining a function that fetches the attributes for the block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was simply the minimum effort for me to make the POC work at the time. We should revisit the "reading" side of the story.


return settings;
}

if ( window.__experimentalConnections ) {
addFilter(
'blocks.registerBlockType',
Expand All @@ -136,4 +198,9 @@ if ( window.__experimentalConnections ) {
'core/connections/with-inspector-control',
withInspectorControl
);
addFilter(
'blocks.registerBlockType',
'core/pattern/shimAttributeSource',
shimAttributeSource
);
}
3 changes: 3 additions & 0 deletions packages/block-library/src/block/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"keywords": [ "reusable" ],
"textdomain": "default",
"attributes": {
"dynamicContent": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we plan to keep it as an experiment, then it should get injected from outside, similar to uses_context for block types that have block bindings defined.

"type": "object"
},
"ref": {
"type": "number"
}
Expand Down
Loading
Loading