Skip to content

Block editor should work in "headless" mode, without Edit UI mounted #59592

@jsnajdr

Description

@jsnajdr

When working on lazy loading block edit functions in #55585, I am dealing with a situation where a block already exists (is part of the block tree in the core/block-editor store), but its <Edit> UI is not yet mounted, because the edit component is being lazy loaded. I am trying to come up with a reasonable "universal edit placeholder" that could be displayed while the real edit is loading, and that could handle keyboard typing in the interim period.

While working on this, one of the surprising findings was that the edit component doesn't only provide UI for editing, but also defines some basic block behavior. In MVC language, it's not only "view", but also implements part of the "model". Architecturally, that's bad. Some examples:

You can't insert an inner block without edit UI mounted: If you dispatch the insertBlocks or replaceBlocks action in the core/block-editor store, and the edit UI is not mounted, these actions will fail, they won't insert any new blocks. Because the canInsertBlockType permission check will return false -- it determines whether the target block supports inner blocks, and it detects that by checking for presence of getBlockListSettings for that block. But block list settings are set by the edit React UI, namely the InnerBlocks component, which calls updateBlockListSettings on mount.

Default templates are not inserted. Many blocks, when inserted from the slash inserter, will not insert just the block itself, but will also create some default inner structure. List has one inner List Item. Quote and Cover has a Paragraph. Social Links have a set of four default social icons. These are inserted only when the Edit UI is mounted. Because the insertion is done by an on-mount effect inside useInnerBlockTemplateSync called in InnerBlocks.

Inserting blocks with prefix transforms (* -> list, > -> quote, ...) does the same job differently -- creating the inner blocks is directly inside the transform functions. Therefore, prefix transforms work correctly when headless.

Splitting and merging list items: List items have custom merge and split behavior, mostly to account for nested sub-lists, but store actions like mergeBlocks don't know about it because it's defined inside the edit UI and passed as props to RichText. Ideally, all this behavior should be present in the merge function defined on the block type registration.

Then the merging and splitting could be done headlessly, without edit UI mounted. After all, the core/block-editor store fully owns and manages the selection, i.e., it knows where the text cursor is, and doesn't need any edit UI for that. The edit UI should only bind the split/merge operations to keys like Enter or Backspace.

Block bindings: We are in the middle of developing block attribute bindings, but are we really doing it right? Block bindings should be 100% a "data problem" -- a block attribute is not stored in the block markup as usual, but is sourced from some other data source... That should be happening solely somewhere inside the core/block-editor store, shouldn't it? The actual edit UI shouldn't need to be aware where the data come from. And yet we have React components for data synchronization like BlockBindingBridge or BindingConnector. That looks suspicious.

Metadata

Metadata

Assignees

No one assigned

    Labels

    [Type] DiscussionFor issues that are high-level and not yet ready to implement.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions