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

Can ACF support the native render callback in block.json? #730

Open
CreativeDive opened this issue Oct 10, 2022 · 19 comments
Open

Can ACF support the native render callback in block.json? #730

CreativeDive opened this issue Oct 10, 2022 · 19 comments

Comments

@CreativeDive
Copy link
Collaborator

CreativeDive commented Oct 10, 2022

Hello @lgladdy,

I read about it here: WordPress/gutenberg#42430

It seems that block.json supports a render callback by default.

src/block.json

{
    "name": "test/my-block",
    "render": "file:./render.php"
}

src/render.php

<div <?php echo get_block_wrapper_attributes(); ?>>
    <?php echo $attributes['text']; ?>
</div>

I'm wondering if we can use this render callback instead of ACF's own "renderTemplate": "index.php" in the future if ACF supports it?

Could this be a solution to make ACF block markup identical in editor and frontend? 👈 👈 👈

Have you ever been able to test whether this can replace the additional block wrapper?

@lgladdy
Copy link
Member

lgladdy commented Oct 10, 2022

The upcoming render property in WordPress 6.1 is for the <ServerSideRender /> component as detailed here: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-server-side-render/

It's not applicable to ACF Blocks, also note:

ServerSideRender should be regarded as a fallback or legacy mechanism, it is not appropriate for developing new features against.

@lgladdy lgladdy closed this as completed Oct 10, 2022
@lgladdy lgladdy closed this as not planned Won't fix, can't repro, duplicate, stale Oct 10, 2022
@CreativeDive
Copy link
Collaborator Author

@lgladdy Thanks for the clarification. I thought this might be a way to solve this issue with the extra wp block wrapper div. Too bad it's not like that.

@lgladdy
Copy link
Member

lgladdy commented Oct 12, 2022

@CreativeDive reopening this one as it looks like this might not be the case after all from the 6.1 block api changes guide: https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/

I’m not sure we’d really get any benefit from supporting it, but we really should take a look at least!

@lgladdy lgladdy reopened this Oct 12, 2022
@CreativeDive
Copy link
Collaborator Author

@lgladdy nice to know. My hope was that we can eliminate the extra div around the ACF block template in the editor based on this example:

<div <?php echo get_block_wrapper_attributes(); ?>>
    <?php echo $attributes['text']; ?>
</div>

If that would work, then it would finally be done and you could do without hacky workarounds for the correct display in the block editor.

@lgladdy
Copy link
Member

lgladdy commented Oct 13, 2022

@CreativeDive I'd be very surprised if they've done anything different to us! get_block_wrapper_attributes() will just auto-build a style="" and class="" for you based on the border/padding/color etc you pick from support options; you can use that already in ACF Blocks.

You might well be able to test this before me - but you'll need to make a standard non-acf block here to see what markup you get back!

@CreativeDive
Copy link
Collaborator Author

@lgladdy I'm absolutely not familiar with the native react code, but I've tried to test it in this way:

block.json

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "wphave/non-acf",
	"title": "hello",
	"icon": "index-card",
	"category": "design",
	"supports": {
        	"align": [
			"wide",
			"full"
		]
	},
	"render": "file:./render.php",
	"editorScript": "file:./block.js"
}

block.js

( function ( blocks, element, serverSideRender, blockEditor ) {
    var el = element.createElement,
        registerBlockType = blocks.registerBlockType,
        ServerSideRender = serverSideRender,
        useBlockProps = blockEditor.useBlockProps;

    registerBlockType( 'wphave/non-acf', {
        apiVersion: 2,
        title: 'Example',
        icon: 'megaphone',
        category: 'design',

        edit: function ( props ) {
            var blockProps = useBlockProps();
            return el(
                'div',
                blockProps,
                el( ServerSideRender, {
                    block: 'wphave/non-acf',
                    attributes: props.attributes,
                } )
            );
        },
    } );
	
} )(
    window.wp.blocks,
    window.wp.element,
    window.wp.serverSideRender,
    window.wp.blockEditor
);

render.php

<div <?php echo get_block_wrapper_attributes(); ?> id="test">
    PHP RENDER
</div>

It seems this is NOT 😒 a solution to eliminate the extra div.

@CreativeDive
Copy link
Collaborator Author

It seems useBlockProps() have all the attributes attached which makes the block selectable in the editor. Unfortunately, my knowledge of react does not go any further here. But is there a way to pass these attributes to the ACF block wrapper DIV? And if so, would that DIV be the outer DIV at this point?

@CreativeDive
Copy link
Collaborator Author

CreativeDive commented Oct 13, 2022

Theoretically it works, but practically not! 😅 No way to get this block selectable in this way.

block.json

( function ( blocks, element, serverSideRender, blockEditor ) {
    var el = element.createElement,
        registerBlockType = blocks.registerBlockType,
        ServerSideRender = serverSideRender,
        useBlockProps = blockEditor.useBlockProps;	
	
    registerBlockType( 'wphave/non-acf', {
        apiVersion: 2,
        title: 'Example',
        icon: 'megaphone',
        category: 'design',
        attributes: {
                blockId: {
                        type: 'string',
                        default: ''
                }
        },

        edit: function ( props ) {
			
                var blockProps = useBlockProps();			
                props.attributes.blockId = blockProps.id;
            
                return el( ServerSideRender, {
                        block: 'wphave/non-acf',
                        attributes: props.attributes,
                } );
        },
    } );
	
} )(
    window.wp.blocks,
    window.wp.element,
    window.wp.serverSideRender,
    window.wp.blockEditor
);

render.php

<div tabindex="0" id="<?php echo $attributes['blockId']; ?>" role="document" aria-label="Block: Example" data-block="<?php echo str_replace('block-', '', $attributes['blockId']); ?>" data-type="wphave/non-acf" data-title="Example" class="block-editor-block-list__block wp-block wp-block-wphave-non-acf">
    PHP RENDER
</div>

@cabrailsford
Copy link

cabrailsford commented Oct 20, 2022

@CreativeDive I was playing around with ACF/non-ACF blocks the last few days, and I hit upon a possible way for you to not render the extra inner div in the editor. Mind you, this only works for divs without additional classes, but it could work in some instances for you.

In your render template, you could use:

if ( ! $is_preview ) {
	printf(
		'<div %s%s>',
		get_block_wrapper_attributes(),
		! empty( $block['anchor'] ) ? ' id="' . esc_attr( sanitize_title( $block['anchor'] ) ) . '"' : '',
	);
}

// Block content goes here.

if ( ! $is_preview ) {
	echo '</div>';
}

By doing so, you prevent the inner div from displaying in the editor, but can use the same classes on the frontend and backend (e.g. .wp-block-your-block-name), and utilize the classes supplied to the block from WP.

@CreativeDive
Copy link
Collaborator Author

CreativeDive commented Oct 20, 2022

@cabrailsford Thanks for that. Yes, I'm playing with that too. It's the only solution I see here to eliminate the extra wrapper div. It's very frustrating when you work with nested blocks like:

<WP DIV>
    <ACF DIV>
        block content 
            <ACF InnerBlocks DIV>
                <WP DIV>
                     <ACF DIV>
                        ...

Practically there is always something that doesn't work properly, like in the frontend when working with alignments, flexbox, ...:
I'm very very tired of adding more and more extra css rules just for the block editor 😒

<ACF DIV>
     block content 
        <ACF InnerBlocks DIV>
           <ACF DIV>
                ...

I've been looking for a way to resolve this different markup for a long time, right @lgladdy 😅

But it is not possible, at least for ACF, to eliminate this extra div. Your mentioned way is basically a good way but there are side effects like, you can't add PHP generated class names to the <WP DIV>. And other side effects I mentioned here: #739

A JS filter that allows us to add PHP generated class names to the <WP DIV> would be very helpful here, but is it technically possible @lgladdy ?

@CreativeDive
Copy link
Collaborator Author

CreativeDive commented Oct 20, 2022

@cabrailsford Something like this could be working. But this is very limited, because the class names will only outputted to the <WP DIV> if you change a block field.

<?php

$my_acf_block_classes = 'hello world yooo';

$block_id = isset( $block['id'] ) ? $block['id'] : '';
$block_id = str_replace( '_', '-', $block_id );

?>

<script>		
	(function( $ ) {

		function blockClassNames( $block ) {
		
			var block_id = '<?php echo esc_html( $block_id ); ?>';
			var wpBlock = $('#' + block_id);
			var wpBlockClasses = wpBlock.attr('class');
			var acfBlockClasses = '<?php echo esc_html( $my_acf_block_classes ); ?>';	
			
			wpBlock.attr('class', '');
			wpBlock.addClass( wpBlockClasses + ' ' + acfBlockClasses );

		}

		// Adds custom JavaScript to the block HTML
		var initializeBlock = function( $block ) {
			blockClassNames( $block );
		}

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

	})( jQuery );
</script>

@CreativeDive
Copy link
Collaborator Author

@cabrailsford Here is a modified solution to pass the ACF block classes to the parent <WP DIV>. The class names are also present when the editor is initially loaded. However, this is only an experimental solution. At the moment the class names are removed again when you click away from the block.

render.php

Output a hidden <div> in the block editor view only and pass the ACF block classes to the "data-block-acf-class" attribute.

if( is_admin() ) { ?>
	<div data-block-classes="acf-block-classes" data-block-acf-class="hello world yoo" style="display: none"></div>
<?php }

editorScript.js

function blockClassNames( block ) {

	block.each(function() {

		var classes = $(this).find('[data-block-classes="acf-block-classes"]');

		var block_id = $(this).attr('id');
		var wpBlock = $('#' + block_id);
		var wpBlockClasses = wpBlock.attr('class');
		var acfBlockClasses = classes.attr('data-block-acf-class');

		if( ! acfBlockClasses ) {
			return false;
		}

		wpBlock.attr('class', '');
		wpBlock.addClass( wpBlockClasses + ' ' + acfBlockClasses );

	});

}

$(document).on('click', '.acf-block-body', function() {
	blockClassNames( $(this) );
});

// Initialize frontend script AND mobile/tablet preview in block editor
$(document).ready(function() {
	blockClassNames( $('.acf-block-body') );
});

// Adds custom JavaScript to the block HTML
var initializeBlock = function( $block ) {
	blockClassNames( $block );
}

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

Technically, it's similar to getting the class name from the <InnerBlocks /> tag. Therefore it should be possible to build a fully working solution like this to pass the ACF block classes as well.

@lgladdy is this something ACF can provide for us?

@lgladdy
Copy link
Member

lgladdy commented Oct 21, 2022

I don't think so honestly - doing something as custom as this, that modified the actual core output from the block editor in a custom way; not through provided WordPress methods, would mean we'd be building a huge technical debt that we'd have to maintain over every version of WordPress, and risk breaking over even minor version of WordPress.

We support such a wide range of WordPress versions, that it's just not viable for something like this to make it into ACF core.

@CreativeDive
Copy link
Collaborator Author

@lgladdy Yes I can understand that. Can you tell me please what method ACF uses to set the custom class to the <InnerBlocks /> tag?

@lgladdy
Copy link
Member

lgladdy commented Oct 21, 2022

@CreativeDive ACF turns <InnerBlocks /> into a custom functional component called ACFInnerBlocks which uses useInnerBlocksProps to apply all of the core React functionality to our component.

@CreativeDive
Copy link
Collaborator Author

CreativeDive commented Oct 21, 2022

@cabrailsford I've been playing around with react. It seems that using WP React is a more elegant solution here. It's still experimental but looks like a good way to pass ACF block classes to the block wrapper div just using a bit of extra JS here. It works well for this purpose, but could of course be optimized.

render.php

<div data-block-acf-class="<?php echo esc_html( $classes ); ?>" style="display: none"></div>

editorScript.js

var el = wp.element.createElement;
var hoc = wp.compose.createHigherOrderComponent;

var withMyWrapperProp = hoc( function ( BlockListBlock ) {
	
    return function ( props ) {
		
		var acfBlockClasses = '';
		var block_id = 'block-' + props.clientId;
		
		var wpBlock = document.querySelector('#' + block_id);
		
		if( wpBlock ) {
			var acfBlockClasses = wpBlock.querySelector('[data-block-acf-class]');
			if( acfBlockClasses ) {
				acfBlockClasses = acfBlockClasses.getAttribute('data-block-acf-class');
			}
		}
		
        var newProps = {
            ...props,
            wrapperProps: {
                ...props.wrapperProps,
				'className': acfBlockClasses,
            },
        };		
		
        return el( BlockListBlock, newProps );
    };
},
'withMyWrapperProp' );

wp.hooks.addFilter(
    'editor.BlockListBlock',
    'my-plugin/with-client-id-class-name',
    withMyWrapperProp
);

EDIT: And you can use the ACF own "renderTemplate" instead of the new default "render".

EDIT: Some modifications are required to make the JS filter BlockListBlock trigger after every change from the ACF block. Basically it works fine, but sometimes the change is not heard by the filter.

@CreativeDive
Copy link
Collaborator Author

I will end my testing here first. One issue is that the block classes are changed before ACF passes them to the template. This means that the first field change that changes a class name only becomes active with the second field change. I can't get any further here. If anyone is interested and has an idea how to solve this, they are welcome to participate here.

@t-hamano
Copy link

t-hamano commented Jan 18, 2023

Hi @CreativeDive,

I came across this issue by accident, so I will try to write what I know.

As far as I know, as long as the ServerSideRender component is used to render the block on the editor, the extra divs cannot be eliminated. To avoid this, it needs to be implemented in JS instead of using the ServerSideRender component in the editor.

In the dynamic blocks of the core, the ServerSideRender component is not used, except in the older blocks.

I don't know how the ACF block is implemented, but if this component is used for rendering on the editor, it would not be easy to rewrite it into JS. This is because the elements within the block can be defined by the user.

One suggestion would be to apply the new display:contents to the divs that are not needed. It might be possible to treat the div as if it did not exist.

Also, the skipBlockSupportAttributes property of the ServerSideRender component may help prevent extra style output.

@CreativeDive
Copy link
Collaborator Author

CreativeDive commented Sep 9, 2023

In the meanwhile I have now found a way to synchronize the ACF block template wrapper classes with the wrapper div in the editor. It works very well. This allows you to add and remove classes via ACF fields in the block wrapper div.

let addedClasses = {};

const acfSyncBlockWrapperClasses = ( block ) => {

    const acfBlockWrapper = block[0];

    // Search for an uniqe selector of your ACF block template wrapper
    const acfTemplateWrapper = acfBlockWrapper.querySelector('[data-acf-block]');

    if( acfTemplateWrapper ) {

        // If classes were previously added, remove them
        if( addedClasses[acfBlockWrapper] ) {

            addedClasses[acfBlockWrapper].forEach(className => {
                acfBlockWrapper.classList.remove(className);
            });

        }

        // Update the list of recently added classes for this block
        addedClasses[acfBlockWrapper] = Array.from(acfTemplateWrapper.classList);

        // Add the classes from the acfTemplateWrapper to the acfBlockWrapper
        acfBlockWrapper.classList.add(...addedClasses[acfBlockWrapper]);

    }

}

if( window.acf ) {
    acf.addAction('render_block_preview', acfSyncBlockWrapperClasses);     
}

/***/

var el = wp.element.createElement;
var hoc = wp.compose.createHigherOrderComponent;

var withMyWrapperProp = hoc(function(BlockListBlock) {
    return function(props) {
        const block_id = 'block-' + props.clientId;

        // Aktualisieren Sie die Klassen jedes Mal, wenn der Block neu gerendert wird
        var wpBlock = document.querySelector('#' + block_id);

        if (wpBlock) {
            var acfTemplateWrapper = wpBlock.querySelector('[data-acf-block]');

            if (acfTemplateWrapper) {
                // Überschreiben Sie den Cache immer, wenn der Block gerendert wird
                addedClasses[block_id] = Array.from(acfTemplateWrapper.classList);
            }
        }

        var newProps = {
            ...props,
            wrapperProps: {
                ...props.wrapperProps,
                'className': addedClasses[block_id] ? addedClasses[block_id].join(' ') : '',
            },
        };

        return el(BlockListBlock, newProps);
    };
},
'withMyWrapperProp');

wp.hooks.addFilter(
    'editor.BlockListBlock',
    'my-plugin/with-client-id-class-name',
    withMyWrapperProp
);

Maybe this will be very helpful for some people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants