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

Try: Custom block insertion hooks #45098

Open
wants to merge 4 commits into
base: try/example-block-hooks
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { parse, synchronizeBlocksWithTemplate } from '@wordpress/blocks';
import { parse } from '@wordpress/blocks';
import {
createElement,
useMemo,
Expand All @@ -12,12 +12,9 @@ import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
import { PluginArea } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
// @ts-expect-error -- No types for this exist yet.
import { useLayoutTemplate } from '@woocommerce/block-templates';
import { Product } from '@woocommerce/data';
// @ts-expect-error -- No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { store as coreStore } from '@wordpress/core-data';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
Expand All @@ -33,6 +30,9 @@ import {
// It doesn't seem to notice the External dependency block whn @ts-ignore is added.
// eslint-disable-next-line @woocommerce/dependency-group
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
store as coreStore,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore store should be included.
useEntityBlockEditor,
Expand Down Expand Up @@ -80,15 +80,17 @@ export function BlockEditor( {
}, [] );

const productFormTemplates = useSelect( ( select ) => {
// @ts-expect-error No types for this exist yet.
return select( coreStore ).getEntityRecords(
'postType',
'wp_template_part',
{
area: 'product-form',
post_type: 'wp_template_part',
}
) || [];
return (
// @ts-expect-error No types for this exist yet.
select( coreStore ).getEntityRecords(
'postType',
'wp_template_part',
{
area: 'product-form',
post_type: 'wp_template_part',
}
) || []
);
}, [] );

/**
Expand Down Expand Up @@ -179,7 +181,13 @@ export function BlockEditor( {
// the blocks by calling onChange.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ layoutTemplate, settings, productTemplate, productId, productFormTemplates ] );
}, [
layoutTemplate,
settings,
productTemplate,
productId,
productFormTemplates,
] );

// Check if the Modal editor is open from the store.
const isModalEditorOpen = useSelect( ( select ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,65 @@ private function create_new_block_template( $template_file, $template_type, $tem
$after_block_visitor = null;
$hooked_blocks = get_hooked_blocks();
if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) {
$before_block_visitor = make_before_block_visitor( $hooked_blocks, $template );
$after_block_visitor = make_after_block_visitor( $hooked_blocks, $template );
$before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, array( $this, 'insert_blocks' ) );
$after_block_visitor = make_after_block_visitor( $hooked_blocks, $template, array( $this, 'insert_blocks' ) );
}
$blocks = parse_blocks( $template->content );
$template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );

return $template;
}

/**
* Returns the markup for blocks hooked to the given anchor block in a specific relative position.
*
* @since 6.5.0
* @access private
*
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param string $relative_position The relative position of the hooked blocks.
* Can be one of 'before', 'after', 'first_child', or 'last_child'.
* @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position.
* @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to.
* @return string
*/
public function insert_blocks( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) {
/**
* Filters the list of inserted blocks for a given anchor block type and relative position.
*
* @param array[] $inserted_blocks The list of inserted blocks.
* @param string $parsed_anchor_block The parsed anchor block.
* @param string $relative_position The relative position of the hooked blocks.
* Can be one of 'before', 'after', 'first_child', or 'last_child'.
* @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type,
* or pattern that the anchor block belongs to.
*/
$inserted_blocks = apply_filters( "inserted_blocks", array(), $parsed_anchor_block, $relative_position, $context );

$markup = '';
foreach ( $inserted_blocks as $inserted_block ) {
$parsed_inserted_block = array(
'blockName' => $inserted_block['blockName'] ?? '',
'attrs' => $inserted_block['attrs'] ?? array(),
'innerBlocks' => $inserted_block['innerBlocks'] ?? array(),
'innerContent' => $inserted_block['innerContent'] ?? array(),
);

// Parse these first so markup is generated from all inner blocks.
$first_chunk = $this->insert_blocks( $parsed_inserted_block, 'first_child', $hooked_blocks, $context );
$last_chunk = $this->insert_blocks( $parsed_inserted_block, 'last_child', $hooked_blocks, $context );
array_unshift( $parsed_inserted_block['innerContent'], $first_chunk );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This needs further investigation to make sure this won't break things. I suspect this could be problematic depending on existing chunks in innerContent.

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 you need to prepend/append the hooked block to innerBlocks rather than innerContent:

Suggested change
array_unshift( $parsed_inserted_block['innerContent'], $first_chunk );
array_unshift( $parsed_inserted_block['innerBlocks'], $first_chunk );

Additionally, you also need to insert a null in the corresponding position in innerContent, so it should actually read

Suggested change
array_unshift( $parsed_inserted_block['innerContent'], $first_chunk );
array_unshift( $parsed_inserted_block['innerBlocks'], $first_chunk );
array_unshift( $parsed_inserted_block['innerContent'], null );

The way it works is that innerContent can have items that are either HTML strings, or null to mark a position where an item from innerBlocks needs to be interpolated. E.g.

$group_block_with_some_inner_block = array(
	'blockName' => 'core/group',
	'attrs'     => array(),
	'innerBlocks' => array( $some_inner_block ),
	'innerContent' => array(
		'<div class="wp-block-group">',
		null,
		'</div>'
	),
);

Copy link
Contributor

@ockham ockham Feb 27, 2024

Choose a reason for hiding this comment

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

This is still not the full picture BTW if you want to account for some subtleties: As the Group block example demonstrates, you might not actually want to put the null marker for first_child in the very first position in innerContent (or the one for last_child in the very last one), as you might want to account for the wrapping HTML:

$group_block_with_some_inner_block = array(
	'blockName' => 'core/group',
	'attrs'     => array(),
	'innerBlocks' => array( $hooked_first_child, $some_inner_block ),
	'innerContent' => array(
		null, // $hooked_first_child is going to be rendered outside the wrapping div :/
		'<div class="wp-block-group">',
		null,
		'</div>'
	),
);

Instead, you'll likely want to find the first (last) non-HTML location in innerContent. For the original Block Hooks implementation (in GB's WP 6.4 compat shim), we ended up with the following logic: https://github.com/WordPress/gutenberg/blob/98552b2486b52e4dd3c90775c2a029b95e64253c/lib/compat/wordpress-6.4/block-hooks.php#L157-L182

array_push( $parsed_inserted_block['innerContent'], $last_chunk );

// Prepend with blocks inserted before, serialize this block, and append with blocks inserted after.
$markup .= $this->insert_blocks( $parsed_inserted_block, 'before', $hooked_blocks, $context );
$markup .= serialize_block( $parsed_inserted_block );
$markup .= $this->insert_blocks( $parsed_inserted_block, 'after', $hooked_blocks, $context );
}

return $markup;
}

/**
* Get the block template title.
*
Expand Down