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

Experiment: Auto-inserting blocks on the frontend #50103

Closed
wants to merge 39 commits into from

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Apr 26, 2023

What?

Early-stages experiment to explore concepts discussed in #39439.

Why?

See #39439. In short, block themes are currently lacking extensibility; the concept of hooks and filters from PHP themes doesn't carry over to them, so we need a more block-centric concept instead.

How?

Per discussion in #39349, we'd likely want blocks to be auto-inserted as the (previous or next) sibling of a given block, or as its (first or last) child. In this PR, we're demonstrating one example of each sibling and child insertion:

  • We use the render_block hook to insert the Social Icons block as the next sibling of the Post Content block.
  • We use the render_block_data hook (which -- unlike the render_block hook -- conveniently gives us access to a given block's children) to insert the Avatar block as the last child of the Comment Template block.

The latter example is chosen to also demonstrate that block context is successfully passed to auto-inserted blocks (as evidenced by the auto-inserted Avatar blocks showing each comment author's avatar).

This PR required some preparation in order to make the Comment Template block work with auto-inserted blocks, see in particular #50279, #50879, and #50883. (Similar modifications were applied to the Post Template block, see #50313.)

Testing Instructions

  • Use the Twenty Twenty-Three theme.
  • On a single post page, you should see the Social Icon block (showing the WordPress logo) below the post (as sort of a stand-in for a Like button 😅)
  • Have a look at comments below a given post. You should see an Avatar block showing the comment author's avatar (as a stand-in for a Comment Like button)

Screenshots or screencast

image

@ockham
Copy link
Contributor Author

ockham commented Apr 26, 2023

Originally, I wanted to experiment with auto-inserting the Log-in/out block as the last child of the Navigation block. But of course, it's not that easy 😬 The Navigation block, if unmodified by the user, defaults to the Page List block, or renders a previously created navigation menu. This means that the location where we'd want to insert the Log-in block becomes a moving target; and due to the mechanism used by the Navigation block, it also doesn't show up as the Page List blocks (or navigation menu's) parent block.

tl;dr If we want a mechanism that auto-inserts blocks as the last (or first) child of a given parent block, it won't "just work" with the Navigation block; we'll have to manually tweak some of its internals.

@ockham
Copy link
Contributor Author

ockham commented Apr 26, 2023

I chose the example of auto-inserting a Like button (of sorts) below each comment based on what this Jetpack Module does, using the comment_text filter (just to note that this is based on a real-world example).

lib/experimental/blocks.php Outdated Show resolved Hide resolved
@gziolo gziolo added [Feature] Block API API that allows to express the block paradigm. [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible labels Apr 26, 2023
@ockham
Copy link
Contributor Author

ockham commented Apr 27, 2023

Another thing that occurred to me while working on this is that we'll want a way to pass block context (from containing blocks) to auto-inserted blocks. Think of the example of an auto-inserted Like button for comments: That button will need to know which comment is being liked. That information (commentId context) is readily available from the containing comments-template -- we just need to make sure the containing block receives it.

Hopefully, block context is enough information for auto-inserted blocks to work in the desired settings. I'm somewhat optimistic, since each auto-inserted block will also have to work when manually inserted (or persisted) in the editor. The latter will also probably be a good guideline to look at when deciding which information a given auto-inserting block needs.

@ockham
Copy link
Contributor Author

ockham commented Apr 27, 2023

P.S. to the above: Maybe we should make a minimal plugin with a real-world example (the Comments Like button block comes to mind; or maybe a "Share this comment on tumblr" or something) to test our explorations and tentative auto-inserting blocks code with.

@gziolo
Copy link
Member

gziolo commented May 2, 2023

Regarding the block context, passing it to the auto-inserted block could be handled similarly, as we can observe it for the Post Template and Comment Template blocks. In there, the block context changes programmatically depending on the currently iterated entity:

https://github.com/WordPress/gutenberg/blob/f795ebee6977df1fc0b3578297c39e9bba1ebb91/packages/block-library/src/post-template/index.php#L81L98

P.S. to the [above]a(#50103 (comment)): Maybe we should make a minimal plugin with a real-world example (the Comments Like button block comes to mind; or maybe a "Share this comment on tumblr" or something) to test our explorations and tentative auto-inserting blocks code with.

It sounds like a great idea to exercise the assumptions with real-world examples 👍🏻

I don't know which block would be the vest to start with, so any option is fine based on your preference.

@ockham
Copy link
Contributor Author

ockham commented May 2, 2023

I don't know which block would be the vest to start with, so any option is fine based on your preference.

I ended up using the Avatar block (instead of a newly written block) to experiment with (92c29fe). It conveniently uses commentId context, so we can use it as an easy way of visually determining whether or not it is getting the correct context by checking if the auto-inserted avatar below a given comment (rounded corners) matches the avatar that's displayed as part of the comment template for that comment (completely round). Well, it doesn't work out-of-the-box -- note that the first Avatar block with rounded corners right below the first comment doesn't show Wapuu, unlike the matching comment:

image

Regarding the block context, passing it to the auto-inserted block could be handled similarly, as we can observe it for the Post Template and Comment Template blocks. In there, the block context changes programmatically depending on the currently iterated entity: [...]

That was my initial thinking as well. However, it's not as straight-forward, unfortunately; I spent all afternoon trying to make it work; to no avail, so far 😕

While we can easily replace the (context-ignorant) way of rendering our auto-inserted block with the method you're suggesting...

diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php
index 018ae7d2cc..63565d9420 100644
--- a/lib/experimental/blocks.php
+++ b/lib/experimental/blocks.php
@@ -147,7 +147,12 @@ function gutenberg_auto_insert_blocks( $block_content, $block ) {
        $inserted_block_markup = '<!-- wp:avatar {"size":40,"style":{"border":{"radius":"10px"}}} /-->';
 
        $inserted_blocks  = parse_blocks( $inserted_block_markup );
-       $inserted_content = render_block( $inserted_blocks[0] );
+       $inserted_content = ( new WP_Block(
+               $inserted_blocks[0],
+               array(
+                       'commentId' => 1. // FIXME: Provide actual context!!!
+               )
+       ) )->render();
 
        if ( isset( $block['parentBlock'] ) && $block_name === $block['parentBlock'] ) {
                if ( 'last-child' === $block_position ) {

...we don't have any way of obtaining the 'right' context here: While we can access $instance->context ($instance being the third argument to the render_block hook), that context is filtered so it only includes the context that's allowed for that block, as specified in its block.json's useContext field. However, we could really be dealing with any block -- e.g. for the Twenty Twenty Three theme, the Comment Template block's only child block is a Columns block (for layout). If we attempt to auto-insert our block after that block and thus access its $instance->context, it won't include commentId 😕


Alternatively, we could try to auto-insert the block from the render_block_data filter when running on its parent, and hope that we'll be able to get the context that the parent provides. Something like...

diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php
index 018ae7d2cc..2a714a3f93 100644
--- a/lib/experimental/blocks.php
+++ b/lib/experimental/blocks.php
@@ -112,6 +112,19 @@ function gutenberg_auto_insert_child_block( $parsed_block, $source_block, $paren
 	if ( isset( $parent_block ) ) {
 		$parsed_block['parentBlock'] = $parent_block->name;
 	}
+
+	if ( 'core/comment-template' === $parsed_block['blockName'] ) {
+		$inserted_block_markup = <<<END
+<!-- wp:social-links -->
+<ul class="wp-block-social-links"><!-- wp:social-link {"url":"https://wordpress.org","service":"wordpress"} /--></ul>
+<!-- /wp:social-links -->'
+END;
+		$inserted_blocks = parse_blocks( $inserted_block_markup );
+
+		$parsed_block['innerBlocks'][] = $inserted_blocks[0]; // last-child
+
+		// TODO: Implement first-child logic
+	}
 	return $parsed_block;
 }
 add_filter( 'render_block_data', 'gutenberg_auto_insert_child_block', 10, 3 );
@@ -168,4 +181,4 @@ function gutenberg_auto_insert_blocks( $block_content, $block ) {
 
 	return $block_content;
 }
-add_filter( 'render_block', 'gutenberg_auto_insert_blocks', 10, 2 );
+//add_filter( 'render_block', 'gutenberg_auto_insert_blocks', 10, 2 );

...would be ideal in a number of ways: The innerBlocks value is used by the WP_Block constructor to construct the corresponding inner_blocks member, which is an instance of the WP_Block_List class, which conveniently includes context information -- which should thus also apply to all newly added inner blocks 🎉

But alas, I've never managed to get this method to work; while I've verified that $parsed_block['innerBlocks'] ends up containing the newly added block, the example I'm giving doesn't render any social icons 😕

@ockham
Copy link
Contributor Author

ockham commented May 2, 2023

Alternatively, we could try to auto-insert the block from the render_block_data filter when running on its parent, and hope that we'll be able to get the context that the parent provides. Something like... [...]

...would be ideal in a number of ways: The innerBlocks value is used by the WP_Block constructor to construct the corresponding inner_blocks member, which is an instance of the WP_Block_List class, which conveniently includes context information -- which should thus also apply to all newly added inner blocks 🎉

But alas, I've never managed to get this method to work; while I've verified that $parsed_block['innerBlocks'] ends up containing the newly added block, the example I'm giving doesn't render any social icons 😕

I now tried

array_unshift( $parsed_block['innerBlocks'], $inserted_blocks[0] );

instead of

$parsed_block['innerBlocks'][] = $inserted_blocks[0];

and that does indeed show the Social Icon -- but it removes the rest of each comment template 😬

image

So it seems to me that the number of elements in a WP_Block_List is fixed or something like that, and that maybe we're missing some method in WP_Block_List that would allow us to append or prepend new elements to it. Or maybe some "ExtensibleArray" trait, if that's a thing in PHP. I'll continue to look into that unless you have an idea right off the top of your head! 😄

@ockham
Copy link
Contributor Author

ockham commented May 3, 2023

I had some partial success: I think I managed to solve the context problem in #50279 🎉

image

(Note the auto-inserted avatar block under the first comment, which correctly displays Wapuu, as opposed to the screenshot in this comment.)

@ockham ockham force-pushed the try/auto-inserting-blocks branch from 92c29fe to 6efe12a Compare May 3, 2023 12:39
@ockham ockham changed the base branch from trunk to update/comment-template-render-context May 3, 2023 12:39
@ockham
Copy link
Contributor Author

ockham commented May 3, 2023

Rebased on #50279.

Base automatically changed from update/comment-template-render-context to trunk May 4, 2023 08:52
ockham added a commit that referenced this pull request May 4, 2023
In the Comment Template block's render callback, instead of creating a new `WP_Block` instance with `commentId` context set, use the `render_block_context` filter to set that context and render the existing `WP_Block` instance.

This approach arguably follows our established patterns with regard to handling block context better. Notably, with the previous approach, we were only setting block context for the Comment Template block itself. In this PR, we extend it to apply to all child blocks, including ones that are dynamically inserted, e.g. via the `render_block` filter. This is relevant for Auto-inserting blocks (see #50103).
@ockham ockham force-pushed the try/auto-inserting-blocks branch from 6efe12a to f4d5f0c Compare May 4, 2023 08:53
@ockham
Copy link
Contributor Author

ockham commented May 4, 2023

Rebased on trunk now that #50279 has been merged.

ockham added a commit that referenced this pull request May 4, 2023
In the Post Template block's render callback, instead of creating a new `WP_Block` instance with `postId` and `postType` context set, use the `render_block_context` filter to set that context and render the existing `WP_Block` instance.

This approach arguably follows our established patterns with regard to handling block context better. Notably, with the previous approach, we were only setting block context for the Post Template block itself. In this PR, we extend it to apply to all child blocks, including ones that are dynamically inserted, e.g. via the `render_block` filter. This is relevant for Auto-inserting blocks (see #50103).

This follows the precedent of the Comment Template block, see #50279.
lib/experimental/blocks.php Outdated Show resolved Hide resolved
@ockham
Copy link
Contributor Author

ockham commented May 4, 2023

Recap of findings

  1. Auto-inserting a block before or after a given block seems doable; the [render_block hook](https://developer.wordpress.org/reference/hooks/render_block/) is a good fit.
  2. Auto-inserting a block as a given block's firstChild or lastChild is trickier. Within the render_block hook, no information is available about the current block's parent, or if it has any siblings to the left or to the right.
  3. Information about the current block's parent is available within the render_block_data hook, so we can pass that information down to render_block by e.g. attacking a parentBlock field to its parsed_block argument. However, it's a bit difficult to figure out in render_block if we're currently dealing with the target parent block's first or last child (which we need to know in order to auto-insert in the correct position).
  4. Better yet, the render_block_data hook has access to the parsed block's innerBlocks -- so we should be able to append or prepend the auto-inserted block to that array. However, this is also not without problems:

To sum up:

Block filters such as render_block and/or render_block_data seem like the right vehicle, but they might need some tweaking. Block rendering logic is complex, especially for "template" blocks that typically have a render callback that sets up some data before using $block->render( "dynamic" => false ) in order to render their inner blocks (typically to use one template, customized with inner blocks in the editor by the user, to render a number of items such as posts or comments).

render_block is a bit more reliable, as it is called from WP_Block::render(), which seems to be invoked almost universally to render a block. render_block_data is invoked from the top-level render_block() function (not to be confused with the render_block filter!), and when a block renders its inner blocks.

In order to get things to work properly, we'll likely have to untangle why blocks that are appended to a parsed block's innerBlocks array in render_block_data aren't being rendered (even with #50279 reverted). We might have to introduce additional arguments to some block filters (although we can't seem to easily add e.g. a parent_block argument to WP_Block::render, due to how it's typically invoked), or maybe additional filters (such as render_first_inner_block and render_last_inner_block).

I don't believe that we need or should tackle auto-insertion right after parsing, i.e. by injecting into the tree of parsed blocks. The reason for that is that that tree doesn't seem to represent the markup on the frontend that well, especially if "template" blocks are involved: E.g. there might be only one Comments Template block in that tree, but on the frontend, its render callback renders comments recursively, passing the correct comment ID as block context each time. This doesn't seem to be something that can be easily represented by solely modifying the tree of parsed blocks.

@gziolo gziolo mentioned this pull request May 8, 2023
6 tasks
@nefeline
Copy link
Contributor

Hi @ockham! Could you please ping me as soon as this experiment is ready for testing? On WooCommerce we want to explore injecting the mini cart block to the header by relying on auto-inserting blocks.

@ockham
Copy link
Contributor Author

ockham commented May 22, 2023

Hi @ockham! Could you please ping me as soon as this experiment is ready for testing? On WooCommerce we want to explore injecting the mini cart block to the header by relying on auto-inserting blocks.

Hey @nefeline! Apologies for the late reply, I just came back from a 2-week vacation.
Anyway, happy to ping you once this is ready for wider testing! I expect it'll take another couple of weeks however to get it into proper shape.

@nefeline
Copy link
Contributor

Apologies for the late reply, I just came back from a 2-week vacation.

No worries: I hope you enjoyed your time off!

happy to ping you once this is ready for wider testing! I expect it'll take another couple of weeks however to get it into proper shape.

Thank you! Looking forward to starting experimenting with those auto-inserting blocks on Woo.

ockham added a commit that referenced this pull request May 24, 2023
In the Post Template block's render callback, instead of creating a new `WP_Block` instance with `postId` and `postType` context set, use the `render_block_context` filter to set that context and render the existing `WP_Block` instance.

This approach arguably follows our established patterns with regard to handling block context better. Notably, with the previous approach, we were only setting block context for the Post Template block itself. In this PR, we extend it to apply to all child blocks, including ones that are dynamically inserted, e.g. via the `render_block` filter. This is relevant for Auto-inserting blocks (see #50103).

This follows the precedent of the Comment Template block, see #50279.
@ockham
Copy link
Contributor Author

ockham commented Jun 1, 2023

Excellent discovery. So it looks like when the inner content chunk is represented as a string, then it's immediately used, but otherwise, the rendering logic falls back to inner_blocks.

Yeah, exactly 👍

So we need to have both shaped correctly so it works as expected. This might require some further investigation as I belive that innerBlocks and innerContent have different shape. You'll be able to see differences in existing tests for the block serialization parsers.

Yeah, I should've added a TODO for this. I didn't look into this right away since it didn't seem to impact auto-insertion, but I agree that we should be consistent here with what innerContent would look like otherwise (i.e. if not auto-inserted). Thank you for the pointer to the tests, those are helpful in determining the required shape!

@ockham
Copy link
Contributor Author

ockham commented Jun 6, 2023

I’m becoming more and more convinced that the syntax we want for auto-insertion needs to involve block patterns rather than just blocks.

I’ve just updated the PR to support a basic syntax in block.json and used it to auto-insert an Avatar block after Post Content:

	"autoInsert": {
		"core/post-content": "after"
	}

The result really doesn’t look great:

image

The reason is that blocks need at least a minor degree of customization by setting attributes — the defaults might be fine for inserting a new instance of the block in the editor, but they just about never cut it on the frontend.

We had those attributes set at a previous stage of the PR, but it was hardwired (inside the render_block filter):

	$inserted_block = array(
		'blockName'    => 'core/avatar',
		'attrs'        => array(
			'size'  => 40,
			'style' => array(
				'border' => array( 'radius' => '10px' ),
			),
		),
		'innerHTML'    => '',
		'innerContent' => array(),
	);

The Problems

Specifying Block Attributes

During my call with @gziolo, we decided to provide attributes in the autoInsert field of block.json. However, I believe we missed what could be very confusing and counter-intuitive to users. Here's one possible way to specify attribute values:

	"autoInsert": {
		"core/post-content": {
			"position": "after",
			"attrs": {
				"size": 40,
				"style": {
					"border": {
						"radius": "10px"
					}
				}
			}
		}
	}

Aside from getting annoyingly verbose for even the most basic of nested block-supports attributes, it's easy to think that these attributes are for the Post Content block -- when really they are for the Avatar block, whose block.json we're editing! While it's possible to come up with variations of this syntax, I don't think that this confusion can be entirely avoided: After all, the auto-inserted block is always implicitly given, whereas the attributes it requires for auto-insertion aren't, and neither is the "anchor" block -- so we'll always have to specify those two, and quite possibly in close proximity to each other.

Inner Blocks

Attributes aren't the only issue with this approach: My original example in this PR was to insert a Social Icon block (rather than an Avatar) after Post Content. I discovered that for styling to be applied properly, I had to render it wrapped in its Social Links parent block:

	$inserted_block_markup = <<<END
<!-- wp:social-links -->
<ul class="wp-block-social-links"><!-- wp:social-link {"url":"https://wordpress.org","service":"wordpress"} /--></ul>
<!-- /wp:social-links -->'
END;

	$inserted_blocks  = parse_blocks( $inserted_block_markup );
	$inserted_content = render_block( $inserted_blocks[0] );

Once again, a fairly basic example -- but one that cannot be carried over to the above block.json syntax 😕

Static Blocks

Finally, as discussed earlier, this approach was never going to work for static blocks -- as we cannot know on the server side what they serialize to, as this requires saving them on the client side.


Proposal

All of the above problems are solved by block patterns, where we simply specify the serialized block markup anyway.

I'm thus thinking to experiment with auto-inserting block patterns as a next step. We already have the concept of contextual block types there -- a concept that can take on different roles (a pattern to transform a block to, or a destination template area). Maybe we can extend that concept further to also encompass anchor blocks (or add a separate field, if that's again too prone to confusion).

register_block_pattern(
    'my-plugin/wordpress-logo',
    array(
        'title'      => __( 'WordPress Logo', 'my-plugin' ),
        'autoInsert' => 'after',
        'blockTypes' => array( 'core/post-content' ),
        'content'    => '<!-- wp:social-links --><ul class="wp-block-social-links"><!-- wp:social-link {"url":"https://wordpress.org","service":"wordpress"} /--></ul><!-- /wp:social-links -->'',
    )
);

cc/ @mtias

@ockham
Copy link
Contributor Author

ockham commented Jun 7, 2023

PR to explore auto-inserting block patterns (per the above): #51294

@ockham
Copy link
Contributor Author

ockham commented Jun 13, 2023

Spin-off PR to explore block auto-insertion in the editor via the REST API: #51449.

@mtias
Copy link
Member

mtias commented Jun 28, 2023

@ockham I don't think this API should deal directly with patterns. You should always be able to leverage wp:pattern (like themes do) if it comes to it, but the bottomline is that auto-inserting blocks should be equivalent to a user inserting the block themselves in the right place — it has to look good out of the box for the cases that justify auto-insertion (a cart in a menu, etc). If it doesn't, the block defaults are probably off, or the use case is not concrete enough. The example of adding an avatar after the content is quite arbitrary and not really reflective of a natural use case.

@ockham
Copy link
Contributor Author

ockham commented Jul 7, 2023

Closing in favor of #51449, which also auto-inserts blocks into the editor.

@ockham ockham closed this Jul 7, 2023
@ockham ockham deleted the try/auto-inserting-blocks branch July 7, 2023 08:27
sethrubenstein pushed a commit to pewresearch/gutenberg that referenced this pull request Jul 13, 2023
* Post Template Block: Set block context via filter

In the Post Template block's render callback, use the `render_block_context` filter to set `postId` and `postType` context, rather than passing that context directly to the `WP_Block` constructor.

This approach arguably follows our established patterns with regard to handling block context better. Notably, with the previous approach, we were only setting block context for the Post Template block itself. In this PR, we extend it to apply to all child blocks, including ones that are dynamically inserted, e.g. via the `render_block` filter. This is relevant for Auto-inserting blocks (see WordPress#50103).

This follows the precedent of the Comment Template block, see WordPress#50279.

Furthermore, add some test coverage to guard against duplicated block-supports class names, which was an issue in a previous iteration of this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block API API that allows to express the block paradigm. [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants