feat(richtext-lexical): unify lexical nested fields with document form state#15683
Draft
feat(richtext-lexical): unify lexical nested fields with document form state#15683
Conversation
Contributor
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10942
Before
Lexical nodes with nested fields (blocks, inline blocks, links, uploads) were fully isolated from the parent document's form system. Each node type managed its own form lifecycle: block and inline block components rendered their own
<Form>component directly on the page, with their owninitialStatebuilt via a dedicatedbuildInitialStateutility, their own state reducer, and their ownonChangehooks to sync field changes back into the Lexical node viaeditor.update(). Links and uploads did the same when their editing drawers opened.This meant every node with sub-fields was effectively a mini-document with its own form context and submission flow that we had to handle manually - completely separate from the parent document form. From the perspective of Payload's form system, these fields didn't exist:
useAllFormFields(),useFormFields(), and other form hooks couldn't see them. They were buried inside the rich text field's serialized JSON value.After
Nested Lexical fields now live in the parent document's form state, just like array fields, standard blocks, and top-level fields. A lexical block's text field sits at
lexicalField.{nodeId}.fieldNamein the same flatFormStatemap. The node ID is the bridge between the freeform Lexical JSON tree and the flat form state - it's the one stable identifier every node with sub-fields already has. Drawers read from and write to the parent form state directly; thedataprop onFieldsDrawerhas been removed entirely.Why
Consistency and simplicity. Nested Lexical fields are Payload fields - they're defined with the same field config, validated the same way, stored the same way. There was no reason for them to be invisible to the rest of the form system. This change makes them first-class fields, which means any component, plugin, or custom hook can read and write Lexical sub-fields using standard form hooks. It also simplifies the client-side architecture: node components no longer need to manage their own state lifecycle,.
How
A new
buildFormStatemethod on theRichTextAdapterinterface lets the Lexical adapter hook into the existing form state pipeline. During server-side form state construction, the adapter walks the serialized Lexical tree, finds nodes with sub-fields via feature-providedgetSubFieldsDataandgetSchemaPathfunctions, and calls the standarditerateFieldsutility to populate form state entries under{richTextPath}.{nodeId}.*. This works similarly to howgenerateSchemaMapalready works for the schema map - just now for form state.On save, a
beforeChangehook merges flat form state values back into the Lexical JSON tree before persistence, using the samegetSubFieldsDatamap. Any feature that registers sub-fields automatically participates in both directions.On the client, node components no longer receive full field data objects from
decorate(). They receive only structural identifiers (blockType,id,relationTo,value) and read user-editable field data from the parent form state via standard hooks.getFields()andgetData()have been renamed togetStaleFields()andgetStaleData(), as form state is the source of truth and the editor state for the node fields will be stale until the form state is synced back, which happens in the beforeChange hook.Links are a special case: as an
ElementNode, Lexical renders their DOM from internal node data. A targeted sync writes field values back to the link node viaeditor.update()on drawer submit, keeping rendering current.