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

How to: Access InnerBlocks.Content in PHP #7247

Closed
manake opened this issue Jun 10, 2018 · 29 comments
Closed

How to: Access InnerBlocks.Content in PHP #7247

manake opened this issue Jun 10, 2018 · 29 comments
Labels
[Feature] Block API API that allows to express the block paradigm. [Feature] Extensibility The ability to extend blocks or the editing experience [Type] Enhancement A suggestion for improvement.

Comments

@manake
Copy link

manake commented Jun 10, 2018

An example request regarding this documentation page: https://github.com/WordPress/gutenberg/tree/master/editor/components/inner-blocks

wp.blocks.registerBlockType( 'example/hello-world', {
	title: 'Hello World',
	icon: 'universal-access-alt',
	category: 'layout',
	attributes: {},
	edit: function(){
		return wp.element.createElement( wp.editor.InnerBlocks );
	},
	save: function(){
		return null;
	},
});

// This function is wrong and needs documentation.
function php_render_block( $attributes, $content = '' ) {
	// #1 How do I access InnerBlocks.Content here?
	// #2 When accessed how do I typically parse all InnerBlocks.Content?
	return '<p>' . $attributes['foo'] . '</p>' . '<pre><code>' . htmlspecialchars( $content ) . '</code></pre>';
}

Similar example:
https://gist.github.com/mattheu/7cf235b4f932de891bc21cb5f3ff3de6

@ajitbohra ajitbohra added the [Type] Help Request Help with setup, implementation, or "How do I?" questions. label Jul 8, 2018
@mattwatsoncodes
Copy link

Hi @manake,

This file in one of my plugins may help you:
https://github.com/mwtsn/secure-blocks-for-gutenberg/blob/master/blocks/secure-block/php/class-secure-block.php

In particular these two blocks of code:

function register_dynamic_block() {
	// Only load if Gutenberg is available.
	if ( ! function_exists( 'register_block_type' ) ) {
		return;
	}
	// Hook server side rendering into render callback
	register_block_type( 'matt-watson/secure-block', [
		'render_callback' => 'matt_watson\secure_blocks_for_gutenberg\matt_watson_secure_blocks_for_gutenberg_render',
	] );
}

Then you can grab the content in this function:

function matt_watson_secure_blocks_for_gutenberg_render( $attributes, $content ) {
	return $content;
}

Note that I use a namespace in the plugin which is needed when calling the function.

@georgestephanis
Copy link
Contributor

@mwtsn The issue is that the InnerBlocks aren't currently passed to the parent block's content -- or rendered even, it seems.

Related: #6751

This is a known issue, I talked to both @aduth and @dmsnell about it last week -- I'm just popping around and documenting on open issues right now.

@manake
Copy link
Author

manake commented Oct 30, 2018

@mattwatsoncodes $content is empty for me.

Another example, in one of my blocks I have multiple InnerBlocks areas and want to save them all:

el( wp.editor.RichText, {
	tagName: 'div',
	format: 'string',
	placeholder: 'Some text',
	value: someAttribute,
	onChange: function( content ) {
		props.setAttributes({ someAttribute: content }); // I can save this in "someAttribute" easily...
	}
} ),
el(wp.editor.InnerBlocks), // ...but how to save this? And then access and render?
el(wp.editor.InnerBlocks), // ...and this?
el(wp.editor.InnerBlocks), // ...and this?

@rameezwp
Copy link

rameezwp commented Nov 2, 2018

Any workaround for accessing InnerBlocks.Content in php yet?

@jjarolim
Copy link

jjarolim commented Nov 2, 2018

@rameezwp We implemented a small polyfill for that problem: Gutenberg hooks into "the_content" to parse the Block-Comments. We also hooked in with preference 0 so that our filter starts first. Then we have a look into the Gutenberg Block Registry, look for our tags with inner content and called the registered callback ourself. Since Gutenberg Block Meta Comments are very structured, the needed regular expressions to identify tags and inner content were found very easely.

We did that only for nested Gutenberg Blocks and left single tags to the Gutenberg filter.

The solution has the benefit that you can work as documented by gutenberg and just turn off your filter as soon as the Gutenberg team has solved that issue.

BR from Salzburg!

@rameezwp
Copy link

rameezwp commented Nov 2, 2018

So how I can actually render innerblocks from php callback?

block.js
save: function(props) { return el( InnerBlocks.Content ); },
php file
function render_block($attrs, $content){ ob_start(); echo $content; return ob_get_clean(); }

Thanks for your help.

dmsnell added a commit that referenced this issue Nov 7, 2018
Attempt three at including positional information from the parse to enable isomorphic reconstruction of the source `post_content` after parsing.

See alternate attempts: #11082, #11309
Motivated by: #7247, #8760, Automattic/jetpack#10256
Enables: #10463, #10108

## Abstract

Add new `innerContent` property to each block in parser output indicating where in the innerHTML each innerBlock was found.

## Status

 - will update fixtures after design review indicates this is the desired approach
 - all parsers passing new tests for fragment behavior

## Summary

Inner blocks, or nested blocks, or blocks-within-blocks, can exist in Gutenberg posts. They are serialized in `post_content` in place as normal blocks which exist in between another block's comment delimiters.

```html
<!-- wp:outerBlock -->
Check out my
<!-- wp:voidInnerBlock /-->
and my other
<!-- wp:innerBlock -->
with its own content.
<!-- /wp:innerBlock -->
<!-- /wp:outerBlock -->
```

The way this gets parsed leaves us in a quandary: we cannot reconstruct the original `post_content` after parsing because we lose the origin location information for each inner block since they are only passed as an array of inner blocks.

```json
{
	"blockName": "core/outerBlock",
	"attrs": {},
	"innerBlocks": [
		{
			"blockName": "core/voidInnerBlock",
			"attrs": {},
			"innerBlocks": [],
			"innerHTML": ""
		},
		{
			"blockName": "core/innerBlock",
			"attrs": {},
			"innerBlocks": [],
			"innerHTML": "\nwith its own content.\n"
		}
	],
	"innerHTML": "\nCheck out my\n\nand my other\n\n"
}
```

At this point we have parsed the blocks and prepared them for attaching into the JavaScript block code that interprets them but we have lost our reverse transformation.

In this PR I'd like to introduce a new mechanism which shouldn't break existing functionality but which will enable us to go back and forth isomorphically between the `post_content` and first stage of parsing. If we can tear apart a Gutenberg post and reassemble then it will let us to structurally-informed processing of the posts without needing to be aware of all the block JavaScript.

The proposed mechanism is a new property as a **list of HTML fragments with `null` values interspersed between those fragments where the blocks were found**.

```json
{
	"blockName": "core/outerBlock",
	"attrs": {},
	"innerBlocks": [
		{
			"blockName": "core/voidInnerBlock",
			"attrs": {},
			"innerBlocks": [],
			"blockMarkers": [],
			"innerHTML": ""
		},
		{
			"blockName": "core/innerBlock",
			"attrs": {},
			"innerBlocks": [],
			"blockMarkers": [],
			"innerHTML": "\nwith its own content.\n"
		}
	],
	"innerHTML": "\nCheck out my\n\nand my other\n\n",
	"innerContent": [ "\nCheck out my\n", null, "\n and my other\n", null, "\n" ],
}
```

Doing this allows us to replace those `null` values with their associated block (sequentially) from `innerBlocks`.

## Questions

 - Why not use a string token instead of an array?
    - See #11309. The fundamental problem with the token is that it could be valid content input from a person and so there's a probability that we would fail to split the content accurately.

 - Why add the `null` instead of leaving basic array splits like `[ 'before', 'after' ]`?
    - By inspection we can see that without an explicit marker we don't know if the block came before or after or between array elements. We could add empty strings `''` and say that blocks exist only _between_ array elements but the parser code would have to be more complicated to make sure we appropriately add those empty strings. The empty strings are a bit odd anyway.

 - Why add a new property?
    - Code already depends on `innerHTML` and `innerBlocks`; I don't want to break any existing behaviors and adding is less risky than changing.
@noel-schenk
Copy link

noel-schenk commented Nov 19, 2018

@dmsnell thank you for the reference. I can see where your solution will lead but wouldn't it be easier to first render the sub HTML content and then passing it's content to the upper levels?

Example:
World
-> House
--> Table

With your solution if you render World you would have to manually render the house and then make a recursive function to render table as in table could be another block an so on. What if the InnerBlocks had an onChange function which would return the content. This content is allready rendered and doesen't has to be checked for not rendered content as it is already done by it's predecessor (for example table).

So your js code for World could look like this:

 el(InnerBlocks,{
     onChange: (value) => {props.setAttributes({innerBlockValueAsString: value})}
})

@chrisvanpatten and @jorgefilipecosta #7785 #11271 #10745 #6751

@dmsnell
Copy link
Member

dmsnell commented Nov 19, 2018

@NoelElias in #10108 I'm exploring a filtering API but you can see there that the inner blocks are already accessible to parent blocks in PHP as rendered HTML.

I think there are two distinct ways we might want to process a block in PHP: before it's turned into HTML - "structural filtering"; after it's turned into HTML. For example, maybe I want to filter out any inner blocks of the type conditional/on-sunday - then I want access to the structure and not just the rendered HTML. On the other hand, maybe I'm concatenating my inner blocks with the normal output but one of them is dynamic - I need to make sure that render/filter gets in before I use the serialized HTML values. Does that make any sense?

Anyway, I'd love to have you share your input in #10108, or at least the PHP-specific bits. There are some trivial things we can do like pass the array of rendered inner blocks to the render_callback but that trivial action also carries non-trivial implications.

@noel-schenk
Copy link

noel-schenk commented Nov 22, 2018

@dmsnell I think this solution works for editing the innerBlock although registering a callback function inside register_block_type would do the job as well. I personally don't like filter functions but that's just my opinion.

This feature will be important but I still think, that having access to the rendered inner blocks inside render_callback is so much more important right now. Without it people will, for example, start hard coding their headings inside their blocks instead using the heading block provided.

Also with your solution, passing the rendered inner blocks to the render_callback won't be a problem anymore as you can edit them with the filter function before they get passed to the render_callback.

@danielbachhuber danielbachhuber added the [Feature] Extensibility The ability to extend blocks or the editing experience label Nov 27, 2018
@designsimply
Copy link
Member

If I am following correctly, the update in https://core.trac.wordpress.org/ticket/45451 to add a filter to override block attributes solves the problems raised here (and noting that update should ship with WordPress 5.1). @manake, if that is not correct, would you mind please leaving me a comment to let me know?

@designsimply designsimply added [Type] Enhancement A suggestion for improvement. [Feature] Block API API that allows to express the block paradigm. and removed [Type] Help Request Help with setup, implementation, or "How do I?" questions. labels Jan 25, 2019
@Tofandel
Copy link

And so how do you use it?...

@aduth
Copy link
Member

aduth commented May 28, 2019

@Tofandel It is exposed as innerBlocks on the render_block_data and render_block filters $block argument:

https://developer.wordpress.org/reference/hooks/render_block_data/
https://developer.wordpress.org/reference/hooks/render_block/

Example:

<?php

add_filter( 'render_block', function( $block_content, $block ) {
	var_export( $block['innerBlocks'] );

	return $block_content;
}, 10, 2 );

@rodrigodagostino
Copy link

Where are $block_content and $block coming from in your example, @aduth ?

@aduth
Copy link
Member

aduth commented Jul 5, 2019

@rodrigodagostino Those are the arguments passed to the filter callback.

https://github.com/WordPress/WordPress/blob/28a8f31ffa9dc238b56eb2413a728e861207b5af/wp-includes/blocks.php#L267-L275

Make sure you include the fourth argument of add_filter, $accepted_args.

https://developer.wordpress.org/reference/functions/add_filter/

@rodrigodagostino
Copy link

@aduth yes, I understand those are the arguments I need to supply, but where am I supposed to get those from inside my render function?

@aduth
Copy link
Member

aduth commented Jul 5, 2019

The inner blocks are not currently exposed on render_callback. I don't recall if this was intentional.

Instead, you can use the above filter and test whether $block['blockName'] is the name of the block you've implemented.

@rodrigodagostino
Copy link

Thanks for your efforts, @aduth , I still do not really understand how to use that filter. I might take a look to it later.
Thanks for your time too.

@flywolfcreative
Copy link

Regardless of this perhaps being an old thread, it still came up high in google for a search I performed to find out how to output InnerBlocks in PHP, and there isn't much else in the way of info on how to do this. So I wanted to share how I got it working...

In your php file for the block, you will have a function register_block_type, and in there is your render_callback. Whatever function your sending your render_callback to, give that function two parameters - $attributes and $content. Your $content should be your InnerBlocks rendering out.

function your_render_callback($attributes, $content) {
  echo $content; // this should be your InnerBlock output.
}

Hope this helps anyone who is slightly lost.

@johnnya23
Copy link

johnnya23 commented Mar 16, 2020

@flywolfcreative or anyone else:
php:

register_block_type('jma-ghb/featued-block', array( 'attributes' => array( ), 'editor_script' => 'jma-ghb-featured-script', 'render_callback' => 'JMA_GHB_featured_callback', ));

and:

function JMA_GHB_featured_callback($x, $y) { ob_start(); echo $y; return ob_get_clean(); }

wordpress gives us this for block.js https://developer.wordpress.org/block-editor/tutorials/block-tutorial/nested-blocks-inner-blocks/:

( function( blocks, element, blockEditor ) { var el = element.createElement; var InnerBlocks = blockEditor.InnerBlocks; blocks.registerBlockType( 'jma-ghb/featued-block', { title: 'Example: Inner Blocks', category: 'layout', edit: function( props ) { return el( 'div', { className: props.className }, el( InnerBlocks ) ); }, save: function( props ) { return el( 'div', { className: props.className }, el( InnerBlocks.Content ) ); }, } ); } ( window.wp.blocks, window.wp.element, window.wp.blockEditor, ) );

how do I modify block.js (I assume this is where my problem is). I have tried various combinations of ServerSideRender and save : ... return null;
but I can't put it all together. In the block editor I see "block renders empty or undefined and I can't drag anything into it.

thanks

@ETVO
Copy link

ETVO commented May 21, 2020

@flywolfcreative
And what do you write in JS? Does your save function return null or InnerBlocks.Content?

@ETVO
Copy link

ETVO commented Aug 9, 2021

@debojyotighosh this is what works for me to use InnerBlocks content and render it using PHP (server-side)

In your registerBlockType JS (focus on the save property):

// JavaScript block register file
registerBlockType(block.name, {
    title: block.title,
    description: block.desc,
    icon: block.icon,
    parent: block.parentName,
    category: block.categ,
    attributes: block.attrs,
  
    edit: () => {
        return block.edit;
    },
  
    save: () => {
        if(!block.usesInner)
            return null;
        else {
            return <InnerBlocks.Content />
        } 
    }
});

You can return null in the save function because your block will be rendered server-side (using PHP). However, when you're using InnerBlocks, the InnerBlocks.Content has to be returned, otherwise it will not be accessible to the PHP rendering function.

Then, in the rendering function, you must use the following:

// PHP block render file
function render_block($attributes, $content) {

    // Normal block attributes
    $title = $attributes['title'];    
    $subtitle = $attributes['subtitle'];

    ob_start(); // Start HTML buffering

    ?>

        <section>
            <div class="container">
                <h2>
                    <?php echo $title; ?>
                </h2>
                <h4>
                    <?php echo $subtitle; ?>
                </h4>

                <div class="content">
                    <?php echo $content; ?>
                </div>
            </div>
        </section>
    <?php

    $output = ob_get_contents(); // collect buffered contents

    ob_end_clean(); // Stop HTML buffering

    return $output; // Render contents
}

@vladoa
Copy link

vladoa commented Nov 14, 2021

How can I access the attributes of a block within InnerBlocks? Let's say contains a core/gallery block inside which I want to render on my own. How can I get the gallery ID or the Image IDs in the server-side render.php?

@StephenBugden
Copy link

Hello, Does anyone have an answer to the last question from @vladoa ?

I'm trying to access the attributes from the innerblocks and can't find a way to do it.

@jjarolim
Copy link

If you can't find any reference to your parent block, you could maybe get the current post content, use parse_blocks to get the whole block structure as an nested array, search for your block id and go from there ...

@StephenBugden
Copy link

Hi @jjarolim,
Thanks for the idea, I'll take a look.
Assume one of the fields in the post content will contain the attributes.

@StephenBugden
Copy link

StephenBugden commented Apr 28, 2022

Hi @jjarolim,

I tried accessing the content via the $content variable and via get_the_content().

In both cases I don't see the attributes.

This is my callback function:

function render_html($attributes, $content) {
	
	ob_start(); ?>
 	  <h1>Attributes</h1>
 	  <h3>The number of columns is <?php echo esc_html($attributes['myColumns']) ?>!</h3>
	   <p>The content is <?php echo esc_html($content) ?>!</p>
	   <p>The content is <?php echo esc_html(get_the_content()) ?>!</p>
	   
 	  <?php 
	   return ob_get_clean();
}

This is was the output
Attributes
The number of columns is 3!
The content is <div class="wp-block-mywebsitename-generic-cards has-3-columns"> <div class="wp-block-blocks-course-team-member"><img src="https://mywebsitename6.local/wp-content/uploads/2022/04/Robert-3.jpg" alt="" class="wp-image-10496"/><h4>Robert</h4><p>Bio</p><div class="wp-block-blocks-course-team-member-social-links"><ul><li data-icon="facebook"><a href="http::/facebook.com"><span class="dashicon dashicons dashicons-facebook"></span></a></li></ul></div></div> <div class="wp-block-blocks-course-team-member"><img src="https://mywebsitename6.local/wp-content/uploads/2022/04/Phil-3.jpg" alt="" class="wp-image-10499"/><h4>Phil</h4><p>Phil's bio</p><div class="wp-block-blocks-course-team-member-social-links"><ul><li data-icon="facebook"><a href="https://facebook.com"><span class="dashicon dashicons dashicons-facebook"></span></a></li><li data-icon="twitter"><a href="https://twitter.com"><span class="dashicon dashicons dashicons-twitter"></span></a></li></ul></div></div> <div class="wp-block-blocks-course-team-member"></div> </div> !

The content is <!– wp:block {"ref":4939} /–> <!– wp:block {"ref":6275} /–> <!– wp:kadence/rowlayout {"uniqueID":"_26eee5-27","columns":1,"colLayout":"equal","inheritMaxWidth":true} –> <div class="wp-block-kadence-rowlayout alignnone"><div id="kt-layout-id_26eee5-27" class="kt-row-layout-inner kt-layout-id_26eee5-27"><div class="kt-row-column-wrap kt-has-1-columns kt-gutter-default kt-v-gutter-default kt-row-valign-top kt-row-layout-equal kt-tab-layout-inherit kt-m-colapse-left-to-right kt-mobile-layout-row kb-theme-content-width"><!– wp:kadence/column {"uniqueID":"_3ab532-7c"} –> <div class="wp-block-kadence-column inner-column-1 kadence-column_3ab532-7c"><div class="kt-inside-inner-col"><!– wp:mywebsitename/generic-cards –> <div class="wp-block-mywebsitename-generic-cards has-3-columns"><!– wp:blocks-course/team-member {"id":10496} –> <div class="wp-block-blocks-course-team-member"><img src="https://mywebsitename6.local/wp-content/uploads/2022/04/Robert-3.jpg" alt="" class="wp-image-10496"/><h4>Robert</h4><p>Bio</p><div class="wp-block-blocks-course-team-member-social-links"><ul><li data-icon="facebook"><a href="http::/facebook.com"><span class="dashicon dashicons dashicons-facebook"></span></a></li></ul></div></div> <!– /wp:blocks-course/team-member –> <!– wp:blocks-course/team-member {"id":10499} –> <div class="wp-block-blocks-course-team-member"><img src="https://mywebsitename6.local/wp-content/uploads/2022/04/Phil-3.jpg" alt="" class="wp-image-10499"/><h4>Phil</h4><p>Phil's bio</p><div class="wp-block-blocks-course-team-member-social-links"><ul><li data-icon="facebook"><a href="https://facebook.com"><span class="dashicon dashicons dashicons-facebook"></span></a></li><li data-icon="twitter"><a href="https://twitter.com"><span class="dashicon dashicons dashicons-twitter"></span></a></li></ul></div></div> <!– /wp:blocks-course/team-member –> <!– wp:blocks-course/team-member –> <div class="wp-block-blocks-course-team-member"></div> <!– /wp:blocks-course/team-member –></div> <!– /wp:mywebsitename/generic-cards –></div></div> <!– /wp:kadence/column –></div></div></div> <!– /wp:kadence/rowlayout –>!

@jjarolim
Copy link

jjarolim commented Apr 28, 2022

What do you get via parse_blocks( get_the_content() ) ? You should get a iterable array with all blocks of the page incl. according attributes?

@StephenBugden
Copy link

StephenBugden commented Apr 30, 2022

Hi,

@jjarolim thank you for your idea, sorry for the delayed response, I had problems with my laptop.

I tried the following:
<h2>The content is </h2><p><?php echo var_dump(parse_blocks( get_the_content() )) ?>!</p>

Output is as follows:
`C:\Users\Steve\Local Sites\mywebsitename6\app\public\wp-content\plugins\mywebsitename-cards\mywebsitename-cards.php:37:
array (size=5)
0 =>
array (size=5)
'blockName' => string 'core/block' (length=10)
'attrs' =>
array (size=1)
'ref' => int 4939
'innerBlocks' =>
array (size=0)
empty
'innerHTML' => string '' (length=0)
'innerContent' =>
array (size=0)
empty
1 =>
array (size=5)
'blockName' => null
'attrs' =>
array (size=0)
empty
'innerBlocks' =>
array (size=0)
empty
'innerHTML' => string '

' (length=2)
'innerContent' =>
array (size=1)
0 => string '

' (length=2)
2 =>
array (size=5)
'blockName' => string 'core/block' (length=10)
'attrs' =>
array (size=1)
'ref' => int 6275
'innerBlocks' =>
array (size=0)
empty
'innerHTML' => string '' (length=0)
'innerContent' =>
array (size=0)
empty
3 =>
array (size=5)
'blockName' => null
'attrs' =>
array (size=0)
empty
'innerBlocks' =>
array (size=0)
empty
'innerHTML' => string '

' (length=2)
'innerContent' =>
array (size=1)
0 => string '

' (length=2)
4 =>
array (size=5)
'blockName' => string 'kadence/rowlayout' (length=17)
'attrs' =>
array (size=4)
'uniqueID' => string '_91026b-c2' (length=10)
'columns' => int 1
'colLayout' => string 'equal' (length=5)
'inheritMaxWidth' => boolean true
'innerBlocks' =>
array (size=1)
0 =>
array (size=5)
...
'innerHTML' => string '

' (length=372) 'innerContent' => array (size=3) 0 => string '
' (length=353) 1 => null 2 => string '
' (length=19)`

So I don't think the attributes are being stored separately and I can't understand why.

Steve

@phpbits
Copy link
Contributor

phpbits commented Jun 5, 2023

This would be the easier way to access the innerblocks data in the block PHP render:

<?php
/**
 * Render Custom Block
 *
 * @param  array    $atts    Block Attributes
 * @param  string   $content Block Content (InnerBlocks)
 * @param WP_Block $block_class Block Class Instance
 * @return string          Block Markup
 */
function render( $atts, $content, $block_class ) {

	// Check how many inner blocks are available
	$item_count = $block_class->inner_blocks->count();

	if ( $item_count < 1 ) {
		return '';
	}
	
	ob_start();
	?>
	<div>
		<?php
			// iterate over the available inner blocks
			for ( $index = 1; $index <= $item_count; $index++ ) :

				// Get the inner block data
				$inner_block = $block_class->inner_blocks->current(); 
				
				// Holds the inner block attribute
				$attribute   = $inner_block->attributes;

				// This will display the attributes data
				var_dump( $attribute );

				// increase the index in the WP_Block_List class used to retrieve the current block
				$block_class->inner_blocks->next();
			endfor;
				// reset the index in the WP_Block_List class to the initial state
				$block_class->inner_blocks->rewind();
			?>
	</div>
	<?php

	return ob_get_clean();
}
?>

Btw, I created a full guide here: https://jeffreycarandang.com/how-to-access-innerblocks-attributes-within-the-parent-block-on-the-wordpress-gutenberg-editor/. Thanks!

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. [Feature] Extensibility The ability to extend blocks or the editing experience [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests