Skip to content

Instantly share code, notes, and snippets.

@gaambo
Last active December 29, 2021 18:11
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gaambo/633bcd83a9596762218ffa65d0cfe22a to your computer and use it in GitHub Desktop.
Save gaambo/633bcd83a9596762218ffa65d0cfe22a to your computer and use it in GitHub Desktop.
ACF Block with Innerblocks
import { Fragment } from "@wordpress/element";
import { InnerBlocks } from "@wordpress/editor";
/**
* Changes the edit function of an ACF-block to allow InnerBlocks
* Should be called like this on `editor.BlockEdit` hook:
* ` addFilter("editor.BlockEdit", "namespace/block", editWithInnerBlocks("acf/block-name"));`
*
* @param {string} blockName the name of the block to wrap
* @param {object} innerBlockParams params to be passed to the InnerBlocks component (like allowedChildren)
*/
const editWithInnerBlocks = (
blockName,
innerBlockParams,
append = true,
hideBlockEdit = false
) => BlockEdit => props => {
if (props.name !== blockName) {
return <BlockEdit {...props} />;
}
if (append) {
return (
<Fragment>
{!hideBlockEdit && <BlockEdit {...props} />}
<InnerBlocks {...innerBlockParams} />
</Fragment>
);
}
// put before block edit
return (
<Fragment>
<InnerBlocks {...innerBlockParams} />
{!hideBlockEdit && <BlockEdit {...props} />}
</Fragment>
);
};
/**
* Changes the save function of an ACF-block to allow InnerBlocks
* Should be called like this on `blocks.getSaveElement` hook:
* `addFilter("blocks.getSaveElement", "namespace/block", saveWithInnerBlocks("acf/block-name"));`
*
* @param {string} blockName the name of the block to wrap
*/
const saveWithInnerBlocks = blockName => (BlockSave, block) => {
if (typeof block === "undefined") {
return BlockSave;
}
if (block.name !== blockName) {
return BlockSave || block.save;
}
return (
<div>
<InnerBlocks.Content />
</div>
);
};
export { editWithInnerBlocks, saveWithInnerBlocks };
@gaambo
Copy link
Author

gaambo commented Aug 9, 2019

My use case is a “Text” Block which wrapps all other textish blocks (paragraph, heading, list) for a common wrapper (container, margins/paddings – all design reasons).
It’s kinda hacky, works only until ACF 5.8.2 (have to find out what’s breaking it in 5.8.3). But with mode set to preview, the fields only showing in the sidebar and some styling I managed to get it working for editors/normal users.

@CreativeDive
Copy link

CreativeDive commented Oct 26, 2019

@gaambo

Thanks for your work. Do you can try to explain me, how I can allow InnerBlocks with your code to a specific ACF block?

What is the namespace in the following filter?

addFilter("editor.BlockEdit", "namespace/header", editWithInnerBlocks("acf/header"));

If I add a filter like:

addFilter("editor.BlockEdit", "acf/section", editWithInnerBlocks("acf/section"));

... nothing happens with my ACF block like:

acf_register_block_type(
			array(
				'name' => 'section',

@gaambo
Copy link
Author

gaambo commented Oct 28, 2019

Usage in the Editor / Backend

The namespace is the second argument according to the documentation about block filters - actually it doesn't really matter. If you use it in a custom theme/plugin it's good if you use your plugin name as the first part. I use the second part (after the /) for the block I'm using it for. So it doesn't have to be the same name as your block.
Only the argument to editWithInnerBlocks needs to be the correct block name (ACF blocks always have acf/ prefixed).

The Gutenberg documentation about block filters also has a concrete example how to add filters via ES5 or ESNext. You just have to put the snippet above in the same file (and remove the exports at the bottom) or import it into your file (eg I put it in a file in a util subfolder and import it in all block-files I need it).
Then you have to build it and enqueue it in the editor (as with all editor assets - see the Gutenberg documentation).

If you're not sure it's getting called correctly you can set a breakpoint in your browsers developer tools and see if editWithInnerBlocks is called.

The innerBlockParams argument is an object of all possible params you can pass to the InnerBlock component - see documentation for more information (eg allowedBlocks, templateLock).

You have to add filters to both editor.BlockEdit and blocks.getSaveElement to ensure the innerblocks content is saved as well.

Usage on the frontend

To output the innerblocks on the frontend you have to use the second argument of the render_callback passed to acf_register_block_type. This argument has all the block contents as HTML string - see ACF documentation

Hope that helps.

@gaambo
Copy link
Author

gaambo commented Oct 28, 2019

Some more notes about the Editor View

The innerBlocks will always be appended or prepended to the original blockedit (the acf fields) - depending on the append parameter you pass to editWithInnerBlocks. Rendering them "inside" the ACF fields is not possible at the moment (and without a big refactor of ACF probably never will, because the fields form is rendered on the server and the rendered html is insert into the block editor as a whole).

An example of how I use it: I've got a "Textmodule" block which allows to add any WordPress core text blocks as innerBlocks but has some additional ACF fields (styling, animations,...). I set the mode to preview in acf_register_block_type so the fields are shown in the sidebar but the innerBlocks are editable in the editor.
Another block has some fields which will be rendered on the side of some core blocks. To make the editing experience as similar as possible to the output on the frontend I add these custom editor styles:

.wp-block[data-type="acf/text"] {
  div[data-block] {
    display: flex;
    flex-direction: row;

    > .acf-block-component {
      flex: 0 0 (100%/3);
      max-width: (100%/3);

      *[class*="col-"] {
        width: 100% !important;
        flex: 1 1 100%;
        max-width: initial;
      }
    }

    > .editor-inner-blocks {
      flex: 0 0 (100%/1.5);
      max-width: (100%/1.5);

      div[data-block] {
        display: block;
      }
    }
  }
}

@CreativeDive
Copy link

CreativeDive commented Nov 5, 2019

@gaambo Thank you, it works very well.

I've used:

wp.hooks.addFilter("editor.BlockEdit", "my_acf_blocks/section", editWithInnerBlocks("acf/section"));
wp.hooks.addFilter("blocks.getSaveElement", "my_acf_blocks/section", saveWithInnerBlocks("acf/section"));

... instead of:

addFilter("editor.BlockEdit", "my_acf_blocks/section", editWithInnerBlocks("acf/section"));
addFilter("blocks.getSaveElement", "my_acf_blocks/section", saveWithInnerBlocks("acf/section"));

this solves the issue.

My result:

import { Fragment } from "@wordpress/element";
import { InnerBlocks } from "@wordpress/editor";

### Extend the ACF blocks with inner blocks:

/**
 * Changes the edit function of an ACF-block to allow InnerBlocks
 * Should be called like this on `editor.BlockEdit` hook:
 * ` addFilter("editor.BlockEdit", "namespace/header", editWithInnerBlocks("acf/header"));`
 *
 * @param {string} blockName the name of the block to wrap
 * @param {object} innerBlockParams params to be passed to the InnerBlocks component (like allowedChildren)
 */

const editWithInnerBlocks = (
  blockName,
  innerBlockParams,
  append = true,
  hideBlockEdit = false
) => BlockEdit => props => {
  if (props.name !== blockName) {
    return <BlockEdit {...props} />;
  }
  if (append) {
    return (
      <Fragment>
        {!hideBlockEdit && <BlockEdit {...props} />}
        <InnerBlocks {...innerBlockParams} />
      </Fragment>
    );
  }
  // put before block edit
  return (
    <Fragment>
      <InnerBlocks {...innerBlockParams} />
      {!hideBlockEdit && <BlockEdit {...props} />}
    </Fragment>
  );
};

/**
 * Changes the save function of an ACF-block to allow InnerBlocks
 * Should be called like this on `blocks.getSaveElement` hook:
 * `addFilter("blocks.getSaveElement", "namespace/header", saveWithInnerBlocks("acf/header"));`
 *
 * @param {string} blockName the name of the block to wrap
 */


const saveWithInnerBlocks = blockName => (BlockSave, block) => {
  if (typeof block === "undefined") {
    return BlockSave;
  }

  if (block.name !== blockName) {
    return BlockSave || block.save;
  }

  return (
    <div>
      <InnerBlocks.Content />
    </div>
  );
};

export { editWithInnerBlocks, saveWithInnerBlocks };

wp.hooks.addFilter("editor.BlockEdit", "wphave/section", editWithInnerBlocks("acf/section"));
wp.hooks.addFilter("blocks.getSaveElement", "wphave/section", saveWithInnerBlocks("acf/section"));

Start npm run build with "create-guten-block" and my ACF section block have a block inserter for awesome inner blocks ... Yeah 👍

Nov-05-2019 21-43-38

How I can show the inner block content on the frontend ACF block?

Add a custom block by acf_register_block_type and use the render_callback. The callback function use the $content variable, which will outputs the inner blocks content 📦

/****************
* SECTION BLOCK (EXPERIMENTAL)
****************/

acf_register_block_type(
	array(
		'name' => 'section',
		'title' => __('Section', 'text-domain') . ' (' . __('Experimental', 'text-domain') . ')',
		'description' => '',
		'icon' => '',
		'category' => '',
		'keywords' => array( 'section', 'wrapper', 'background' ),
		'mode' => 'preview',
		'supports' => array(
			'align' => array( 'center', 'wide', 'full' ),
			'mode' => true,
			'multiple' => true,
		),
		'render_callback'   => 'my_acf_section_block_callback',
	)
);

function my_acf_section_block_callback( $block, $content = '', $is_preview = true, $post_id = 0 ) {

	$block = isset( $block ) ? $block : '';

	$options = array(
		'class' => 'section-block' . ' ' . wphave_block_editor_class( $block ),
	); 

	if( $block ) { 

		/****************
		* SECTION BEFORE
		****************/

		my_block_section_before( $options ); ?>

		<div class="section-block-container">

	<?php }

	// Check for inner blocks
	$inner_blocks = isset( $content ) ? $content : false;

	if( $inner_blocks ) { ?>
		<div class="inner-block">
			<?php 

			/****************
			* INNER BLOCKS
			****************/

			if( $inner_blocks ) {
				echo $inner_blocks;
			} ?>
		</div>
	<?php }

	if( $block ) { ?>

		</div>

		<?php 

		/****************
		* SECTION AFTER
		****************/

		my_block_section_after(); 

	}

}

How it looks on the frontend?

Yeah, the core blocks are nested in my ACF section block. Amazing ✌️

Bildschirmfoto 2019-11-05 um 21 56 07

But wait! The core blocks are positioned below my ACF section block on the backend!

That's not really what I want 🤕

Bildschirmfoto 2019-11-05 um 21 59 01

How I can move the inner blocks inside my ACF section block HTML output?

Unfortunately this is not possible in a simple way. The only way I've found, was to manipulate the rendered backend block with javascript on the ACF block initialization with the following js filter window.acf.addAction( 'render_block_preview/type=section', initializeBlock );

But notice, the following code is very specific on how do you build your ACF block HTML. The following example will move the rendered HTML of my ACF section block from the ".acf-block-preview" container and wrap this HTML around the ".editor-inner-blocks" container after each modification of the ACF wrapper block.

// Define the function
	function buildSectionEditorBlock( $selector ) {
		
		$selector.each( function() {
			
			// Selector
			var wrapper = $(this);
			
			// Wrapper parent element
			var wrapperParent = wrapper.closest('.acf-block-preview');
			
			// Wrapper inner content
			//var wrapperContent = wrapperParent.html();
			
			// Wrapper inner content of ".section-block-container"
			//var wrapperContentInner = wrapperParent.find('.section-block-container').html();
			
			// Define the target element for cloned inner content of the wrapper
			//var wrapperTarget = wrapper.closest('.editor-block-list__block-edit');
	
			// Get ONLY the wrapper container element without inner content
			var wrapperContainer = wrapper.clone().contents().remove().end()[0].outerHTML;
			
			// Get the wrapper background layer inner element
			var wrapperBackgroundLayer = wrapper.find('.bg-layer');
			var wrapperBackgroundLayerHTML;
			if( wrapperBackgroundLayer.length ) {
				wrapperBackgroundLayerHTML = wrapper.find('.bg-layer').get(0).outerHTML;
			}
			
			
			// Define the "inner blocks" area
			var innerBlocks = wrapper.parent().parent().parent().parent().find('.editor-inner-blocks');
						
			/*
			* REMOVE OLD CLONE
			*/
			
			if( innerBlocks.parent().is('.section-block-container') ) {
				
				// Remove all elements between ".section-block" and ".acf-block-component"				
				$('.section-block').prevUntil( ".acf-block-component" ).remove();
				
				innerBlocks.unwrap();
				
			}
			
			// If cloned content already exist, first remove the old cloned content
			if( innerBlocks.parent().is('.section-block') ) {
				
				if( innerBlocks.parent('.section-block').is('.bg-layer') ) {
					innerBlocks.prev().remove();
				}
				innerBlocks.prev().remove();
				innerBlocks.find('.bg-layer').remove();
				innerBlocks.unwrap();
				
			}
			
			/*
			* BUILD
			*/
			
			// Re-Build (clone) the wrapper content
			// First wrap "inner blocks" with the wrapper container
			innerBlocks.wrapAll( wrapperContainer );
			
			// Add the background layer
			if( innerBlocks.parent().is('.section-block') && wrapperBackgroundLayer ) {
				innerBlocks.parent('.section-block').prepend( wrapperBackgroundLayerHTML );
			}
			
			// Wrap the inner content (other inner blocks)
			innerBlocks.wrapAll('<div class="section-block-container"></div>');
			
			// Insert inner content from the wrapper
			/*if( innerBlocks.parent().is('.section-block-container') ) {				
				wrapperTarget.parent().find('.section-block-container').prepend( wrapperContentInner );		
			}*/
			
			/*
			* REMOVE ORIGINAL WRAPPER CONTENT
			*/
			
			//wrapperParent.empty();
			wrapperParent.remove();
			
		});

	}
	
	// Adds custom JavaScript to the block HTML
	var initializeBlock = function( $block ) {
		buildSectionEditorBlock( $block.find('.section-block') );
    }

	// Initialize dynamic block preview (editor)
    if( window.acf ) {
        window.acf.addAction( 'render_block_preview/type=section', initializeBlock );
    }

And how it looks on the backend now?

The block experience on the backend is now the same as the experience on the frontend. 🔨 👍

Nov-05-2019 22-57-18

@maccyd10
Copy link

Is there any possibility of posting a pre-transpiled version that uses wp.element and wp.editor to make it easier for the folks that don't really understand Babel / Webpack and what not?

@CreativeDive
Copy link

Currently there is no way to create a solution out of the box. Maybe @elliotcondon comes with a better solution in the future. A solution which will work fine for all cases.

@gaambo
Copy link
Author

gaambo commented Nov 22, 2019

@CreativeDive thanks for your additions and the extensive documentation - awesome!

@maccyd10 Do you want a Version just with wp.element and wp.editor (because that would be easy to edit, just change the imports to const declarations) or also a version which uses createElement instead of JSX?

@CatinhoCR
Copy link

Awesome. Thank you all, will be implementing during the week and will edit this comment if any contribution comes up but just by reading all this I can tell you both saved me quite some time, just wanted to show the love.

@terence1990
Copy link

terence1990 commented Feb 19, 2020

Hey guys, this I is a standardisation I am using to move Inner Blocks in the editor:

import editWithInnerBlocks from './editWithInnerBlocks'
import saveWithInnerBlocks from './saveWithInnerBlocks'
import moveInnerBlocks from './moveInnerBlocks';

const { addFilter } = wp.hooks

const blocks = acf.data.blockTypes.filter(block => block.has_inner_blocks) // this property explained further down

blocks.forEach(block => {
	addFilter("editor.BlockEdit", `with-inner-blocks/${block.name}`, editWithInnerBlocks(block.name))
	addFilter("blocks.getSaveElement",  `with-inner-blocks/${block.name}`, saveWithInnerBlocks(block.name))
	acf.addAction( `render_block_preview/type=${block.name.replace('acf/', '')}`, (preview) => moveInnerBlocks(preview, block) )
})
// moveInnerBlocks
export default ($preview, block) => {
	
	const preview = $preview[0]	
	const target = preview.querySelector('.js-inner-blocks') // this className explained further down
	
	if( target ) {
		
		// check cached innerBlocks first otherwise we lose them every time we make change to ACF field for the block
		if( block.innerBlocks ) {
			
			target.appendChild(block.innerBlocks)
			
		} else {
			
			const innerBlocks = preview.closest('.wp-block').querySelector('.editor-inner-blocks')
			
			// cache the innerBlocks for later otherwise we lose them every time we make change to ACF field for the block
			block.innerBlocks = innerBlocks 
			
			target.appendChild(innerBlocks)
			
		}
		
	}
	
}

These are my args for these kind of Blocks:

[
    'name' => 'example-block',
    'title' => 'Example Block',
    'category' => 'wp-kit-example-blocks',
    'icon' => 'welcome-widgets-menus',
    'description' => 'An example block',
    'has_inner_blocks' => true,
    'render_callback' => function($block, $inner_blocks) {	
		include(locate_template('views/example.block.php'));
	}
]

By having has_inner_blocks set to true the iteration is handled in Javascript above

Here's my HTML for the block:

// views/example.block.php
<div class="example">
	
	<h1>Hello <?php the_field('text'); ?>!</h1>
	
	<div class="js-inner-blocks">
	
		<?= $inner_blocks; ?>
		
	</div>
	
</div>

I always have a node with js-inner-blocks className wrapping where I want my inner blocks so Javascript above can target it in block editor. For the frontend the $inner_blocks is coming in from second argument of render_callback. Everything looks nice in the backend and the frontend.

I think these kind of standardisation could be worked into ACF directly, the code is not too opinionated.

@gaambo
Copy link
Author

gaambo commented Feb 20, 2020

@terence1990 that looks great. I also thought about having a supports flag innerBlocks and den do everything automatically (also the supported innerblocks etc.). Thanks for your snippet :)

@CreativeDive
Copy link

@terence1990 really cool and thank you for your work. It works like a charm :-)

Please note, since the latest Gutenberg version the inner blocks container selector was changed from .editor-inner-blocks to .block-editor-inner-blocks.

@CreativeDive
Copy link

@gaambo and @terence1990 any idea how we can solve it with multiple inner blocks like different columns inside an ACF block and each column can include different inner blocks? :-)

@CreativeDive
Copy link

CreativeDive commented Mar 30, 2020

@gaambo and @terence1990 an other issue is, if you use the same block multiple times the selector class "js-inner-blocks" works only for on block, but not for multiple blocks. The selector class needs a unique identifier e.g. the ACF block id, but I don't know how I can get the ACF block id inside the react code. Is there a filter of ACF which provides the block id?

@gaambo
Copy link
Author

gaambo commented Apr 1, 2020

Thanks for your inputs - I'll have to test & play with WordPress 5.4 in the following days and hopefully I can come up with an solution or at least an idea - I'll let you know :)

@gaambo
Copy link
Author

gaambo commented Apr 1, 2020

@CreativeDive Regarding multiple inner blocks: AFAIK there's still no way to include multiple innerBlocks in a block (even via React) - so we'd have to gez creative here and a solution should be future-compatible.
Right now the only thing that comes into my mind is building multiple blocks:

  • "Container"/"Wrapper" block which allows only the following "Slots" block
  • "Slots" block which can only be inserted to certain parents and only allows single blocks
  • Single block which can only be inserted in the slots block.

Depending on the use case that's not really easiert then using the core group + columbs blocks.

For accordions I solved it like this:
Accordion-ACF-Block which only allows Accordion-Iten Blocks as innerBlocks.

@gaambo
Copy link
Author

gaambo commented May 29, 2020

@CreativeDive
Copy link

@gaambo: Very exciting ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment