Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

⚛️ Use a wp-inner-block attribute for each inner block #77

Merged
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
91 changes: 60 additions & 31 deletions block-hydration-experiments.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ function block_hydration_experiments_init()

function bhe_block_wrapper($block_content, $block, $instance)
{
// Append the `wp-inner-block` attribute for inner blocks of interactive blocks.
if (isset($instance->parsed_block['isInnerBlock'])) {
$block_content = bhe_append_attributes(
$instance->name,
$block_content,
'wp-inner-block'
);
}

$block_type = $instance->block_type;

if (!block_has_support($block_type, ['view'])) {
Expand Down Expand Up @@ -93,50 +102,70 @@ function bhe_block_wrapper($block_content, $block, $instance)
WP_Block_Supports::$block_to_render = $previous_block_to_render;

// Generate all required wrapper attributes.
$block_wrapper_attributes =
sprintf(
'data-wp-block-type="%1$s" ' .
$block_wrapper_attributes = sprintf(
'data-wp-block-type="%1$s" ' .
'data-wp-block-uses-block-context="%2$s" ' .
'data-wp-block-provides-block-context="%3$s" ' .
'data-wp-block-attributes="%4$s" ' .
'data-wp-block-sourced-attributes="%5$s" ' .
'data-wp-block-props="%6$s" ' .
'data-wp-block-hydration="%7$s"',
esc_attr($block['blockName']),
esc_attr(json_encode($block_type->uses_context)),
esc_attr(json_encode($block_type->provides_context)),
esc_attr(json_encode($attributes)),
esc_attr(json_encode($sourced_attributes)),
esc_attr(json_encode($block_props)),
esc_attr($hydration_technique)
);
esc_attr($block['blockName']),
esc_attr(json_encode($block_type->uses_context)),
esc_attr(json_encode($block_type->provides_context)),
esc_attr(json_encode($attributes)),
esc_attr(json_encode($sourced_attributes)),
esc_attr(json_encode($block_props)),
esc_attr($hydration_technique)
);

// Append block wrapper attributes.
$block_content = bhe_append_attributes(
$instance->name,
$block_content,
$block_wrapper_attributes
);

// The block content comes between two line breaks that seem to be included during block
// The block content comes between line breaks that seem to be included during block
// serialization, corresponding to those between the block markup and the block content.
//
// They need to be removed here; otherwise, the preact hydration fails.
$block_content = substr($block_content, 1, -1);
$block_content = trim($block_content);

// Append all wp block attributes after the class attribute containing the block class name.
// Elements with that class name are supposed to be those with the wrapper attributes.
//
// We could use `WP_HTML_Walker` (see https://github.com/WordPress/gutenberg/pull/42485) in the
// future if that PR is finally merged.

// This replace is similar to the one used in Gutenberg (see
// https://github.com/WordPress/gutenberg/blob/1582c723f31ce0f728cd4dcc1af37821342eeaaa/packages/blocks/src/api/serializer.js#L41-L44).
$block_classname = 'wp-block-' . preg_replace(
['/\//', '/^core-/'],
['-', ''],
$instance->name
);
return $block_content;
}

add_filter('render_block', 'bhe_block_wrapper', 10, 3);

/**
* Add a flag to mark inner blocks of interactive blocks.
*/
function bhe_inner_blocks($parsed_block, $source_block, $parent_block)
{
if (
isset($parent_block) &&
block_has_support($parent_block->block_type, ['view'])
) {
$parsed_block['isInnerBlock'] = true;
}

return $parsed_block;
}

add_filter('render_block_data', 'bhe_inner_blocks', 10, 3);

/**
* Append attributes to the block wrapper element, which is assumed to be the first one.
*
* TODO: use `WP_HTML_Tag_Processor` (see https://github.com/WordPress/gutenberg/pull/42485) once
* the API is released.
*/
function bhe_append_attributes($block_name, $block_content, $attributes)
Copy link
Member

Choose a reason for hiding this comment

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

$block_name doesn't seem to be used anymore.

{
// Be aware that this pattern could not cover some edge cases.
$class_pattern = '/class="\s*(?:[\w\s-]\s+)*' . $block_classname . '(?:\s+[\w-]+)*\s*"/';
$class_replacement = '$0 ' . $block_wrapper_attributes;
$block_content = preg_replace( $class_pattern, $class_replacement, $block_content, 1 );
$pattern = '/^\s*<[^>]+/';
Copy link
Member

Choose a reason for hiding this comment

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

Ok, so this is now looking for the first tag opener, whatever it is, and it could be replaced with this, right?

$w = new WP_HTML_Tag_Processor($block_content);
$w->next_tag();
foreach ($attributes as $key => $value) {
  $w->set_attribute($key, $value);
}

With a bit more logic for the style and class attributes.

$replacement = '$0 ' . $attributes;
$block_content = preg_replace($pattern, $replacement, $block_content, 1);

return $block_content;
}

add_filter('render_block', 'bhe_block_wrapper', 10, 3);
14 changes: 9 additions & 5 deletions src/gutenberg-packages/to-vdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ export default function toVdom(n) {
// Walk child nodes and return vDOM children.
const children = [].map.call(n.childNodes, toVdom).filter(exists);

// Create an array of inner blocks if they are found in children. All nodes in between (e.g.,
// line breaks, white spaces, etc.) are preserved in order to prevent hydration failure.
const isInnerBlock = ({ props }) => props && 'wp-inner-block' in props;
if (children.some(isInnerBlock)) {
Comment on lines +51 to +52
Copy link
Member

Choose a reason for hiding this comment

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

Note for the future: this is probably not the best approach because, although key in obj is fast, it's doing it multiple times.

If we continue with this approach, we could create a nice set of benchmarks for the toVdom function 🙂

const first = children.findIndex(isInnerBlock);
const last = children.findLastIndex(isInnerBlock);
innerBlocksFound = children.slice(first, last + 1);
Copy link
Member

Choose a reason for hiding this comment

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

Note for the future: As we are not doing out-of-order hydration for the moment, we could bypass the toVdom processing of the client component inner nodes.

}

// Add inner blocks.
if (wpBlock.type && innerBlocksFound) {
wpBlock.innerBlocks = innerBlocksFound;
Expand All @@ -58,11 +67,6 @@ export default function toVdom(n) {
// Create vNode. Note that all `wpBlock` props should exist now to make directives work.
const vNode = h(type, props, children);

// Save a renference to this vNode if it's an <wp-inner-blocks>` wrapper.
if (type === 'wp-inner-blocks') {
innerBlocksFound = vNode;
}

return vNode;
}

Expand Down
19 changes: 8 additions & 11 deletions src/gutenberg-packages/wordpress-blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@ const Wrapper =
(Comp) =>
({ attributes }) =>
(
<>
{/* Block Context is not available during save
https://wordpress.slack.com/archives/C02QB2JS7/p1649347999484329 */}
<Comp
blockProps={useBlockProps.save()}
attributes={attributes}
context={{}}
>
<wp-inner-blocks {...useInnerBlocksProps.save()} />
</Comp>
</>
// Block Context is not available during save
// https://wordpress.slack.com/archives/C02QB2JS7/p1649347999484329
<Comp
blockProps={useBlockProps.save()}
attributes={attributes}
context={{}}
{...useInnerBlocksProps.save()}
/>
);

export const registerBlockType = (name, { edit, view, ...rest }) => {
Expand Down