feat(richtext-lexical): add view override system for custom node rendering#14244
Conversation
…This will reduce the size of the lexical field editor property in the schema
📦 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.
|
| errorCount, | ||
| toggleDrawer, | ||
| clientBlock?.fields, | ||
| // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing. |
There was a problem hiding this comment.
Doesn't need formData anymore, This comment is outdated. Manually verified it doesn't kick the cursor out anymore
|
|
||
| ## Views | ||
|
|
||
| Views allow you to customize how Lexical nodes render in both the admin panel and frontend. You can define multiple named views (like `default`, `frontend`, `debug`) and switch between them using the built-in view selector. |
There was a problem hiding this comment.
I think I wouldn't use the word "frontend" so much, or perhaps I would clarify "jsx converter, for example, on the frontend," since there are many ways and converters to use Lexical on the frontend. It would also be good to know how it's handled if someone modifies jsx converters and views. Who wins?
The same applies to the rest of the docs.
| admin: { | ||
| hideGutter: true, | ||
| }, |
There was a problem hiding this comment.
I think it would be better not to make these properties part of the API. Let users use CSS instead; it's more flexible and clear. Or, if for some reason it's needed programmatically, I suggest adding a field for inline styles in the container.
What do you think?
There was a problem hiding this comment.
I think we should keep it, because some properties there are difficult to adjust with css.
E.g. hideGutter - the CSS shouldn't need to only hide the gutter, it also needs to adjust paddings for things like the contenteditable placeholder text.
| createDOM(args) { | ||
| const { node } = args | ||
| const heading = document.createElement(node.getTag()) | ||
| heading.style.color = '#3b82f6' | ||
| heading.style.borderBottom = '2px solid #60a5fa' | ||
| return heading |
There was a problem hiding this comment.
Is this type-safe? If so, how?
There was a problem hiding this comment.
It's just typed as HTMLElement. No more type-safe than the lexical createDOM
| }, | ||
| blocks: { | ||
| myBlock: { | ||
| Component: ({ node, isEditor, isJSXConverter }) => { |
There was a problem hiding this comment.
What are isEditor and isJSXConverter? and it is node type-safe (a BlockNode)?
There was a problem hiding this comment.
Yes, node is fully-typed:
The view map can be shared in both admin panel and frontend. The Component function is called from both. isEditor will be true in the admin panel, isJSXConverter will be true in the frontend.
Some of the args are different depending on whether it's used in frontend or admin panel. E.g. the frontend doesn't have access to the useBlockComponentContext arg
| const mergedConverters = nodeMap | ||
| ? { | ||
| ...converters, | ||
| ...nodeMapToConverters(nodeMap), | ||
| } | ||
| : converters |
There was a problem hiding this comment.
This is a shallow merge instead of a deep one. Won't it cause problems if both parties define blocks or inlineBlocks?
| views, | ||
| } = props | ||
| const { currentView } = useRichTextView() | ||
| const currentViewAdminConfig: LexicalFieldAdminClientProps = views?.[currentView]?.admin ?? _admin |
There was a problem hiding this comment.
If a parent forces a view like frontend, nested editors can still end up ignoring their own default view. RichTextViewProvider falls back to views.default (L105 in next file), but the field/editor code reads views[currentView] directly, so the UI says it's on the inherited view while the child skips its fallback config and node overrides.
There was a problem hiding this comment.
Good catch, fixed it. The provider now falls back currentView itself if the view is not in the view map. So views[currentView] should always be valid
|
🚀 This is included in version v3.83.0 |
…ering (payloadcms#14244) Adds support for custom view maps that allow users to override how Lexical nodes are rendered in the editor. This enables full control over node presentation without modifying the underlying data structure or node class.⚠️ **Experimental**: This API is experimental and may change in minor releases. ## Key Features - **Custom Node Rendering**: Override the visual presentation of any node type (built-in or custom) - **Per-Editor Configuration**: Each editor instance can have its own view overrides. Each editor can toggle between multiple views, with each view rendering nodes differently. - **Dual Usage**: View definitions work in both the admin panel editor and frontend JSX serialization, ensuring visual consistency ## Benefits - **WYSIWYG Editing**: Make the admin editor look exactly like your frontend - **Consistent Rendering**: Single source of truth for both admin and frontend ## Usage Define a view map and pass it to the lexical editor: ```tsx // views.tsx export const myViews: LexicalEditorViewMap = { default: { // Override heading rendering with custom DOM heading: { createDOM() { const h2 = document.createElement('h2') h2.textContent = 'Custom Heading' return h2 }, }, // Override with a React component horizontalRule: { Component: () => <div className="custom-hr">---</div>, }, // Override with HTML string/function link: { html: '<a href="#">Custom Link</a>', }, }, } ``` ```ts // config.ts { fields: [ { name: 'content', type: 'richText', editor: lexicalEditor({ views: '/path/to/views.tsx#myViews', }), }, ] } ```
Adds support for custom view maps that allow users to override how Lexical nodes are rendered in the editor. This enables full control over node presentation without modifying the underlying data structure or node class.
Key Features
Benefits
Usage
Define a view map and pass it to the lexical editor: