Blocks: Add innerContent support for static inner blocks, adopt it in the HTML block#79115
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +1.28 kB (+0.02%) Total Size: 7.51 MB 📦 View Changed
|
| - _name_ `string`: Block name. | ||
| - _attributes_ `Record< string, unknown >`: Block attributes. | ||
| - _innerBlocks_ `Block[]`: Nested blocks. | ||
| - _innerContent_ `Array< string | null >`: Static HTML fragments interleaved with inner blocks, where `null` entries mark inner block positions. Only applies to blocks with the `innerContent` support. |
There was a problem hiding this comment.
Is innerBlocks useful when innerContent is true? Can't we just replace that instead of doing a new arg?
There was a problem hiding this comment.
It is useful and in fact it help us keep all the things working properly: things like list views for instance.
innerContent is just the extra HTML fragments.
There was a problem hiding this comment.
This name doesn't seem very self-evident for what it does (block islands), can we think of something else? Perhaps something like innerBlockIslands or innerBlockSlots
There was a problem hiding this comment.
Happy to change the name, but it's not just the slots, it's also the other unrelated HTML content. The slots are represented with null values.
There was a problem hiding this comment.
One argument for keeping innerContent is that this is basically an information that we already had in the "raw parser" and the property is named innerContent there too.
| import BlockListBlock from '../block-list/block'; | ||
| import { LayoutProvider } from '../block-list/layout'; | ||
|
|
||
| const SLOT_TAG_NAME = 'wp-inner-block-slot'; |
| ```js | ||
| supports: { | ||
| // Serialize from static HTML fragments interleaved with inner blocks. | ||
| innerContent: true |
There was a problem hiding this comment.
Where do you see this being used outside the html block?
There was a problem hiding this comment.
I don't but didn't really want to hard code the HTML block in the parser... potentially someone can build. custom HTML block even if the value is very low.
|
Thanks for running with this idea. I think it is quite nice. The mental model it imposes is the same as a locked / content-only pattern from the perspective of what is editable and what block tools are available: designated blocks nodes can be edited but they cannot be moved, etc. We got there in two ways — by locking down everything that is not deemed editable, or by starting with a non-editable block (html) and marking parts of it editable. There are probable some nuances we are missing, including how basic tools like "word count", outline, etc, might operate when there are chunks of content that are not visible as block tree nodes. So I wouldn't say this replaces those mechanisms entirely, but it gives a lot of flexibility to build content without depending on a top-down block tree. |
… the HTML block Introduce a new block support, innerContent, that lets a block keep static HTML fragments interleaved with inner blocks as the canonical source of its own markup: - The parser retains the grammar's innerContent array (static fragments with null placeholders marking inner block positions) on the parsed block, and skips save-based validation since the content round-trips by construction. - The serializer emits opted-in blocks by interleaving the fragments with their serialized inner blocks instead of calling save. - A new private InnerContent block-editor component renders the static markup inert (no script execution, inline handlers stripped) and portals each inner block into its placeholder position, where it is editable in place but locked against moving, removing, and inserting. No alignment options are offered within static markup. The Custom HTML block adopts the support unconditionally: the content attribute is removed (serialized output is byte-identical for existing content, so no deprecation is needed), the canvas SandBox is replaced with InnerContent, the code modal re-parses on update so wp:* delimited segments become editable inner blocks, and listView support surfaces those inner blocks in the inspector. Co-Authored-By: Claude Fable 5 <[email protected]>
Replace the templateLock 'all' that InnerContent set on the parent's block list settings with innerContent-aware checks in canRemoveBlock, canMoveBlock, and canInsertBlockType. Template locks inherit downward, which wrongly blocked insertion inside containers (e.g. a Group block) nested within an HTML block. The constraint now applies only to the direct children, which are fixed at their placeholder positions; deeper descendants edit normally. Co-Authored-By: Claude Fable 5 <[email protected]>
The button borrowed the block inspector's edit-contents class names for styling, but those are owned by the framework's EditContents component, which renders above the inspector tabs where a zeroed top margin is intentional. Style the button in the block's own editor.scss instead, with spacing on all sides since it renders inside the tabs panel, and revert the scoped override added to the block inspector stylesheet. Co-Authored-By: Claude Fable 5 <[email protected]>
The code block's FROM HTML transform test seeded the HTML block via the removed content attribute, producing an empty block; seed it through serialized markup instead. Also deflake the new HTML inner blocks test: wait for the paragraph to be focused before typing, and assert the lock through the options menu and toolbar instead of a swallowed click. Co-Authored-By: Claude Fable 5 <[email protected]>
Bail out of the modal update when the content is unchanged, and keep the existing inner blocks (and thereby their client IDs and selection) when only the surrounding static HTML was edited, so the mounted blocks aren't needlessly replaced. Co-Authored-By: Claude Fable 5 <[email protected]>
6427bd8 to
6362673
Compare
Co-Authored-By: Claude Fable 5 <[email protected]>
|
I think this is ready for a final review/approval if we want to move forward with it. |
The HTML block's markup now lives in innerContent rather than the content attribute, so update the collaboration gauntlet to insert and edit it through innerContent and assert on it via the full block data. Add crdt-blocks unit tests confirming innerContent round-trips through the CRDT merge (it flows through the generic property handling). Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
Flaky tests detected in 6caef11. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27963377728
|
The code-to-HTML transform dumped the entire source into a single static inner content fragment, so any block delimiters became inert comment text rather than editable inner blocks. This broke the html -> code -> html round trip. Re-parse the source so delimiters become inner blocks at their positions, mirroring the code modal. Co-Authored-By: Claude Opus 4.8 <[email protected]>
parse() already yields a fully-formed core/html block with the right inner blocks and inner content, so wrapping it in another createBlock is redundant. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Passing innerContent for a block that lacks the innerContent support silently dropped it, which wasn't obvious. Emit a development warning via @wordpress/warning so the dependency on the block support is clear. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the generic innerContent block support with a hardcoded core/html check in the parser, serializer, createBlock, and the block-editor locking selectors. Other blocks could otherwise opt into bypassing validation and deprecations, which mtias and tyxla flagged as undesirable. Remove the support from block.json, the BlockSupports type, the JSON schema, and the docs. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The crdt-blocks tests assign innerContent on Block literals, so add the field to the local Block interface. Fixes the TypeScript build. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
So should we give this a try? |
|
I would love an approval here, I don't want to miss the 7.1 deadline haha |
|
I'm merging this soon unless there are any blockers that are raised. |
|
Catching up with this; thanks for the exploration, @youknowriad! I'm picking up on your "for now" regarding restricting this to the HTML block: I've been exploring PHP-only blocks for #79330 on top of this, so the core/html hardcode closes that path. By hardcoding core/html, a block registered in PHP can no longer use this. Is that the final call, or could PHP-registered blocks also opt into the same behavior, even if only later? Should I explore a similar approach without depending on this PR? |
Why would a block registered in PHP want to use this? I don't understand the relationship? On the other hand I think what we should allow after this PR, is that users can just register a variation of an HTML block where you actually provider the innerContent. I'm not sure the current variations API allows this yet. |
I was thinking of the PHP-only follow-up in #79330 and the goal of making them editable in the canvas rather than using a render_ballback, with one open question being whether we could leverage this directly instead of building on top of patterns. So far, the pattern approach looks good and is simpler (although both work well), so I'm not sure this alternative approach needs to solve that, but it was worth mentioning. |
|
@youknowriad I'm in favor of this. I'm just not sure about the timing stuff... When you say
I'm very much on board and think we should ship it.... When you then say you want to merge it now so that it gets into the beta cutoff for 7.1 which gets released in basically a month I'm less sure. This is a major change in how the concept of html parsing / validation etc is handled in the editor and there is no way back from here when we ship it in core. So ideally I'd want us all to be really sure that this is "ready" and I'm not sure we will get that feedback in 2 weeks 🤔 But I won't block merging here. Just sharing my thoughts :) |
|
We have two weeks in Gutenberg and whole beta/RC cycle. It is meaningful for sure but conceptually it has proven to be a logical continuation of what we had, so I don't think it's a huge change really. |
I think that indeed they allow a similar behavior (editable islands in a middle of a locked "block") but conceptually they are very different. the pattern for instance can't be LLM friendly while the HTML block support for things like global styles... are not given. So each has its pros and cons but I don't see one using the other. |
What?
Introduces a new block support,
innerContent, that lets a block keep static HTML fragments interleaved with inner blocks as the canonical source of its own markup — and adopts it unconditionally in the Custom HTML block. This makes it possible to have editable blocks (a paragraph, an image…) at arbitrary positions deep inside an otherwise static HTML structure, without them being movable or removable like regular inner blocks.Why?
Complex hand-written HTML structures currently force a choice: either everything is static (HTML block) or everything is blocks. There's no way to mark just parts of an HTML structure as editable. The block grammar has always supported interleaved HTML and blocks (
innerContentwithnullplaceholders), but the in-memory model, serializer, and editor never exposed it for valid registered blocks.How?
@wordpress/blocks— the data layer:innerContentarray on parsed blocks with theinnerContentsupport and skipssave-based validation: serializing the parsed content reproduces the input by construction.save(inner blocks without a matching placeholder are defensively appended so content is never lost).createBlock()accepts an optional fourthinnerContentargument;getBlockFromExample()passes it through.WP_Block::render()already interleavesinner_contentwith rendered inner blocks, so no PHP changes are needed.@wordpress/block-editor— a new privateInnerContentcomponent:innerHTMLnever executes scripts; inlineon*handlers are stripped) and portals each inner block'sBlockListBlockinto a<wp-inner-block-slot>placeholder at its position within the static markup.templateLock: 'all'): no moving, removing, or inserting.Custom HTML block:
innerContentmode. Thecontentattribute (source: raw) is removed — serialized output is byte-identical for existing content, so no deprecation or migration is needed.SandBoxiframe is replaced byInnerContent. Note: scripts no longer execute in the canvas preview (styles still apply); the modal's preview keeps theSandBox.<!-- wp:* -->delimited segments in the HTML tab become editable inner blocks.listViewsupport is enabled so the inner blocks tree shows in the inspector, acting as a navigation aid for the editable parts (appender/reorder/delete stay disabled via the locking).core/htmlcreation sites updated (paste handler, the various convert-to-HTML flows, legacy widget transform); the widget editors' freeform-handler path parses and serializes back unchanged.Testing Instructions
npm run devand start wp-env.Testing Instructions for Keyboard
🤖 Generated with Claude Code