WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.15k stars 4.05k forks source link

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

Open jsnajdr opened 4 months ago

jsnajdr commented 4 months ago

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.

youknowriad commented 4 months ago

I agree with your assessments here and would for us to make improvements here. Ideally "edit" should be about rendering the edit only and not about enabling actions/selectors to work properly.

Block bindings: That should be happening solely somewhere inside the core/block-editor store, shouldn't it?

This is the only point that I disagree with in this issue, I think block bindings are already represented in the core/block-editor store using the "source" attributes of the blocks, the same way there's a "ref" attribute for instance in a template part but the template part's properties are not part of the "block-editor" store.

retrofox commented 4 months ago

Thanks for opening this discussion đź’Ż

And yet we have React components for data synchronization like BlockBindingBridge or BindingConnector. That looks suspicious.

I also agree. Introducing these components was mainly part of a refactoring process. Everything should happen and be addressed independently of the visual representation of the block in the canvas context or wherever it happens.

How to sync actions affect app history is slightly secondary but must be handled carefully. What happens when a new value from an external source updates the bound attribute value? I guess the short answer is that the attribute changes should be registered—something we explored in this (closed) PR.

retrofox commented 4 months ago

If it should be happening somewhere inside the core/block-editor store, does it mean that when the value of the external prop changes, the app should update the data store for all block instances with an attribute bound to it?

retrofox commented 3 months ago

Super draft PR that implements the data propagation in the Mode, out of the context of the View.