-
-
Save gaambo/633bcd83a9596762218ffa65d0cfe22a to your computer and use it in GitHub Desktop.
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 }; |
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',
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.
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;
}
}
}
}
@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 👍
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 ✌️
But wait! The core blocks are positioned below my ACF section block on the backend!
That's not really what I want 🤕
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. 🔨 👍
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?
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.
@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?
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.
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.
@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 :)
@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
.
@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? :-)
@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?
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 :)
@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.
FYI: ACF 5.9 will support innerBlocks: https://www.advancedcustomfields.com/blog/acf-5-9-exciting-new-features/
@gaambo: Very exciting ;-)
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.