gutenberg icon indicating copy to clipboard operation
gutenberg copied to clipboard

RawHTML adds a wrapper div when it shouldn't

Open philipjohn opened this issue 6 months ago • 10 comments

Description

The RawHTML component should only create a wrapper div when there are props other than children to use in that div:

https://github.com/WordPress/gutenberg/blob/b8c1e6b01759a3e209701a3ba96cf75f14b4f1f9/packages/element/src/raw-html.js#L11-L12

However, this is not the behaviour in practice. Take this example Edit function and the editor output:

edit.js:

export default function Edit() {
	const someHtmlContent = '<p>This is some <strong>HTML</strong> content that will be rendered in the editor.</p><p>There should be no wrapping <code>div</code>.</p>';
	return (
		<RawHTML children={ someHtmlContent } />
	);
}

editor output:

Image

I confirmed this with a fresh environment using wp-env and a basic plugin created from create-block. All the code is in this gist: https://gist.github.com/philipjohn/6a18b582d58c2214535ec368d77cea39

I noticed that the code comment says serialize.js will strip the div but I don't see how that's invoked anywhere, so maybe that's the issue.

https://github.com/WordPress/gutenberg/blob/b8c1e6b01759a3e209701a3ba96cf75f14b4f1f9/packages/element/src/raw-html.js#L30-L31

Step-by-step reproduction instructions

  1. Create a new directory on your local machine and copy the files from the gist. Note that -- in filenames represents folders.
  2. Start the environment with wp-env and ensure the plugin is active with wp-env run cli wp plugin activate example-static
  3. Create a new post and insert the Example Static block
  4. Observe that the HTML output of the block includes a wrapping div

Screenshots, screen recording, code snippet

No response

Environment info

WordPress 6.8.1 Twenty Twenty-One 2.5 Gutenberg: not installed

Please confirm that you have searched existing issues in the repo.

  • [x] Yes

Please confirm that you have tested with all plugins deactivated except Gutenberg.

  • [x] Yes

Please confirm which theme type you used for testing.

  • [x] Block
  • [ ] Classic
  • [ ] Hybrid (e.g. classic with theme.json)
  • [ ] Not sure

philipjohn avatar Jun 06 '25 09:06 philipjohn

Hi, I can confirm div element is present when using RawHTML without extra props alongside children in edit.js and was able to reproduce the issue from given code.

Image

While testing, I came accross that, the renderElement for stripping div in RawHTML executes if we are using it in save.js.

Code

export default function save() {
  const someHtmlContent = "<p>I am calling this from save.js</p>";
  return (
    <div {...useBlockProps.save()}>
      <RawHTML children={someHtmlContent} />
    </div>
  );
}

Screenshots

Image

USERSATOSHI avatar Jun 06 '25 12:06 USERSATOSHI

One thing to note that I should have realised was wrong is that this isn't the right way to pass children:

<RawHTML children={ someHtmlContent } />

This is the proper way:

<RawHTML>{ someHtmlContent }<RawHTML/>

It makes no difference to the outcome but I thought I'd mention.

philipjohn avatar Jun 06 '25 13:06 philipjohn

There seems to be a caveat that the div is only skipped when RawHTML is used as part of the block save implementation, but it'll still be present as part of the edit implementation.

The comment there mentions serialize.js: https://github.com/WordPress/gutenberg/blob/b8c1e6b01759a3e209701a3ba96cf75f14b4f1f9/packages/element/src/raw-html.js#L30-L31

This refers to the element package's renderToString implementation which is used for serializing blocks via the save function, this is the line of code that handles not rendering the wrapper: https://github.com/WordPress/gutenberg/blob/b8c1e6b01759a3e209701a3ba96cf75f14b4f1f9/packages/element/src/serialize.js#L572-L584

When blocks are rending in the editor, they're rendered using react's normal ReactDOM renderer, and won't use this renderToString code at all, so react will just render the div that the RawHTML component returns.

I can take a guess at why it was implemented like this - it probably pre-dates React supporting wrapper-less components. This was a workaround for avoiding unnecessary wrappers in saved block content. It might be possible now to move that code to the RawHTML component itself and have it return a fragment when there are no props.

A note though - your block not having a wrapper in the editor might cause some unexpected side effects like drag and drop not working, possibly other issues too. Blocks have wrappers (with blockProps) so that the editor knows where the boundaries between blocks are and the UI works as expected. The editor might even add its own wrapper if you try omitting one, I'm not sure.

talldan avatar Jun 09 '25 03:06 talldan

I see @gziolo and @andrewserong worked on this most recently, so I thought I'd ping them to see if they have any thoughts.

talldan avatar Jun 09 '25 03:06 talldan

In the edit implementation, you always need a block wrapper that receives the props that control block selection, block toolbar, etc. So in the example above, there should be useBlockProps usage:

import { useBlockProps } from '@wordpress/block-editor';
import { RawHTML } from '@wordpress/element';

export default function Edit() {
	const blockProps = useBlockProps();   
	const someHtmlContent = '<p>This is some <strong>HTML</strong> content that will be rendered in the editor.</p><p>There should be no wrapping <code>div</code>.</p>';
	return (
		<RawHTML ...blockProps>{ someHtmlContent }</RawHTML>
	);
}

Using RawHTML in this context would be equivalent to:

	return (
		<div ...blockProps dangerouslySetInnerHTML: { __html: someHtmlContent } />
	);

The HTML snippet provided contains two nodes:

Image

so it wouldn't be compatible with how the block editor expects blocks to be represented during editing.

gziolo avatar Jun 09 '25 06:06 gziolo

your block not having a wrapper in the editor might cause some unexpected side effects like drag and drop not working

I guess, but if the RawHTML block is supposed to omit the wrapping div - as it states in it's own documentation - then those side effects should be dealt with by whatever block/component is using the RawHTML element, not by RawHTML itself. As it stands the output is contrary to what is stated and so it breaks other things like CSS layouts because the output differs between edit and save.

you always need a block wrapper that receives the props that control block selection

Sure, but if I want to output raw html somewhere within my block, RawHTML is breaking that output by adding a wrapping div when I use it in a way that the docs say it should not add a wrapping div.

I think we ought to focus on what RawHTML should be doing rather than how it might be used in implementation. It's implementation should be governed by what the expected behaviour of RawHTML is.

philipjohn avatar Jun 09 '25 11:06 philipjohn

It might be possible to refactor RawHTML to support the use case explained in the editing context, but that would require some operations to overcome React's limitation where you can't dangerously set inner HTML into the React Fragment. In effect, it should be something like:

const parentNode = document.createElement( 'div' );
parentNode.innerHTML = '<p>This is some <strong>HTML</strong> content that will be rendered in the editor.</p><p>There should be no wrapping <code>div</code>.</p>';
const elements  = [];
for ( let i = 0; i <  parentNode.childNodes.length; i++ ) {
    const TagName = parentNode.childNodes[i].nodeName.toLowerCase();
    const html = parentNode.childNodes[i].innerHTML;
    elements.push(  <TagName dangerouslySetInnerHTML: { __html: html } /> );
}
return <>{ elements }</>;

I haven't tested it, but that might potentially work.

gziolo avatar Jun 09 '25 13:06 gziolo

It might be possible to refactor RawHTML to support the use case explained in the editing context, but that would require some operations to overcome React's limitation where you can't dangerously set inner HTML into the React Fragment.

That's a good point, I didn't consider that fragments might have that limitation. That solution is clever, but for me, I don't like adopting that level of complexity in core, and it's not really clear to me that there's a widespread use case.

I expect this might be an unpopular suggestion with @philipjohn, but perhaps it's best to update the docs for RawHTML to mention the limitation and that can be considered the way to solve this issue. The snippet from @gziolo could be a workaround for users that could be mentioned in the docs (if it works). I expect there aren't many people that would need it.

Also have to consider that many users of this component are already living with the limitation and changing the behavior for them might not be ideal.

talldan avatar Jun 10 '25 08:06 talldan

I think this is "won't fix" due to backward compatibility. We could update the JSDoc block to clarify behavior.

Mamaduka avatar Jun 12 '25 06:06 Mamaduka

I wonder if it ever worked without adding a wrapping div? Given someone, at some point, wrote out that comment it would suggest it did so it'd be good to figure out what changed. Or maybe it just never did work without adding a wrapper.

If there isn't a viable solution to remove the wrapper div I think we do just need to update the comment to be clear that a wrapper will be added. The workaround suggestion would be nice, though not crucial.

philipjohn avatar Jun 17 '25 14:06 philipjohn

I did some digging, and I doubt that RawHTML with no wrapper ever worked with React DOM rendering, even before Gutenberg was merged into WordPress #6044. The JSDoc block seems to be a leftover from the initial prototype.

Mamaduka avatar Jun 25 '25 16:06 Mamaduka