Skip to content

Commit 519bb79

Browse files
authored
feat(richtext-lexical): fully-typed blocks in JSX serializer (#9554)
This allows for full type-safety when using our official JSX converter with blocks: ![CleanShot 2024-11-26 at 21 35 18@2x](https://github.com/user-attachments/assets/70ceb3e9-d5d1-4074-a5dd-bb9d514dc229) ![CleanShot 2024-11-26 at 21 35 25@2x](https://github.com/user-attachments/assets/5100133c-8a91-4cfe-8e44-c091b2d86ffa)
1 parent b47ebb6 commit 519bb79

File tree

9 files changed

+62
-42
lines changed

9 files changed

+62
-42
lines changed

packages/richtext-lexical/src/exports/react/components/RichText/converter/defaultConverters.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { DefaultNodeTypes } from '../../../../../nodeTypes.js'
12
import type { JSXConverters } from './types.js'
23

34
import { BlockquoteJSXConverter } from './converters/blockquote.js'
@@ -11,7 +12,7 @@ import { TableJSXConverter } from './converters/table.js'
1112
import { TextJSXConverter } from './converters/text.js'
1213
import { UploadJSXConverter } from './converters/upload.js'
1314

14-
export const defaultJSXConverters: JSXConverters = {
15+
export const defaultJSXConverters: JSXConverters<DefaultNodeTypes> = {
1516
...ParagraphJSXConverter,
1617
...TextJSXConverter,
1718
...LinebreakJSXConverter,

packages/richtext-lexical/src/exports/react/components/RichText/converter/types.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,53 @@ export type JSXConverter<T extends { [key: string]: any; type?: string } = Seria
1919
}) => React.ReactNode[]
2020
parent: SerializedLexicalNodeWithParent
2121
}) => React.ReactNode
22-
export type JSXConverters<T extends { [key: string]: any; type?: string } = DefaultNodeTypes> = {
22+
23+
export type JSXConverters<
24+
T extends { [key: string]: any; type?: string } =
25+
| DefaultNodeTypes
26+
| SerializedBlockNode<{ blockName?: null | string; blockType: string }> // need these to ensure types for blocks and inlineBlocks work if no generics are provided
27+
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>, // need these to ensure types for blocks and inlineBlocks work if no generics are provided
28+
> = {
2329
[key: string]:
2430
| {
25-
[blockSlug: string]: JSXConverter<any> // Not true, but need to appease TypeScript
31+
[blockSlug: string]: JSXConverter<any>
2632
}
2733
| JSXConverter<any>
2834
| undefined
2935
} & {
30-
[nodeType in NonNullable<T['type']>]?: JSXConverter<Extract<T, { type: nodeType }>>
36+
[nodeType in Exclude<NonNullable<T['type']>, 'block' | 'inlineBlock'>]?: JSXConverter<
37+
Extract<T, { type: nodeType }>
38+
>
3139
} & {
3240
blocks?: {
33-
[blockSlug: string]: JSXConverter<{ fields: Record<string, any> } & SerializedBlockNode>
41+
[K in Extract<
42+
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
43+
? B extends { blockType: string }
44+
? B['blockType']
45+
: never
46+
: never,
47+
string
48+
>]?: JSXConverter<
49+
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
50+
? SerializedBlockNode<Extract<B, { blockType: K }>>
51+
: SerializedBlockNode
52+
>
3453
}
3554
inlineBlocks?: {
36-
[blockSlug: string]: JSXConverter<{ fields: Record<string, any> } & SerializedInlineBlockNode>
55+
[K in Extract<
56+
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
57+
? B extends { blockType: string }
58+
? B['blockType']
59+
: never
60+
: never,
61+
string
62+
>]?: JSXConverter<
63+
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
64+
? SerializedInlineBlockNode<Extract<B, { blockType: K }>>
65+
: SerializedInlineBlockNode
66+
>
3767
}
3868
}
39-
4069
export type SerializedLexicalNodeWithParent = {
4170
parent?: SerializedLexicalNode
4271
} & SerializedLexicalNode

packages/richtext-lexical/src/exports/react/components/RichText/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@ import type { SerializedEditorState } from 'lexical'
22

33
import React from 'react'
44

5+
import type {
6+
DefaultNodeTypes,
7+
SerializedBlockNode,
8+
SerializedInlineBlockNode,
9+
} from '../../../../nodeTypes.js'
510
import type { JSXConverters } from './converter/types.js'
611

712
import { defaultJSXConverters } from './converter/defaultConverters.js'
813
import { convertLexicalToJSX } from './converter/index.js'
914

10-
export type JSXConvertersFunction = (args: { defaultConverters: JSXConverters }) => JSXConverters
15+
export type JSXConvertersFunction<
16+
T extends { [key: string]: any; type?: string } =
17+
| DefaultNodeTypes
18+
| SerializedBlockNode<{ blockName?: null | string; blockType: string }>
19+
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>,
20+
> = (args: { defaultConverters: JSXConverters<DefaultNodeTypes> }) => JSXConverters<T>
1121

1222
type Props = {
1323
className?: string

packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import './index.scss'
3939

4040
import { v4 as uuid } from 'uuid'
4141

42-
import type { InlineBlockFields } from '../nodes/InlineBlocksNode.js'
42+
import type { InlineBlockFields } from '../../server/nodes/InlineBlocksNode.js'
4343

4444
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
4545
import { useLexicalDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDrawer.js'

packages/richtext-lexical/src/features/blocks/client/nodes/InlineBlocksNode.tsx

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,22 @@
11
'use client'
2-
import type {
3-
EditorConfig,
4-
LexicalEditor,
5-
LexicalNode,
6-
SerializedLexicalNode,
7-
Spread,
8-
} from 'lexical'
2+
import type { EditorConfig, LexicalEditor, LexicalNode } from 'lexical'
93

104
import ObjectID from 'bson-objectid'
115
import React, { type JSX } from 'react'
126

13-
import type { SerializedServerInlineBlockNode } from '../../server/nodes/InlineBlocksNode.js'
7+
import type {
8+
InlineBlockFields,
9+
SerializedInlineBlockNode,
10+
} from '../../server/nodes/InlineBlocksNode.js'
1411

1512
import { ServerInlineBlockNode } from '../../server/nodes/InlineBlocksNode.js'
1613

17-
export type InlineBlockFields = {
18-
/** Block form data */
19-
[key: string]: any
20-
//blockName: string
21-
blockType: string
22-
id: string
23-
}
24-
2514
const InlineBlockComponent = React.lazy(() =>
2615
import('../componentInline/index.js').then((module) => ({
2716
default: module.InlineBlockComponent,
2817
})),
2918
)
3019

31-
export type SerializedInlineBlockNode = Spread<
32-
{
33-
children?: never // required so that our typed editor state doesn't automatically add children
34-
fields: InlineBlockFields
35-
type: 'inlineBlock'
36-
},
37-
SerializedLexicalNode
38-
>
39-
4020
export class InlineBlockNode extends ServerInlineBlockNode {
4121
static clone(node: ServerInlineBlockNode): ServerInlineBlockNode {
4222
return super.clone(node)
@@ -55,7 +35,7 @@ export class InlineBlockNode extends ServerInlineBlockNode {
5535
return <InlineBlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
5636
}
5737

58-
exportJSON(): SerializedServerInlineBlockNode {
38+
exportJSON(): SerializedInlineBlockNode {
5939
return super.exportJSON()
6040
}
6141
}

packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Block } from 'payload'
22

33
import type { PopulationPromise } from '../../typesServer.js'
4-
import type { SerializedInlineBlockNode } from '../client/nodes/InlineBlocksNode.js'
4+
import type { SerializedInlineBlockNode } from '../server/nodes/InlineBlocksNode.js'
55
import type { SerializedBlockNode } from './nodes/BlocksNode.js'
66

77
import { recursivelyPopulateFieldsForGraphQL } from '../../../populateGraphQL/recursivelyPopulateFieldsForGraphQL.js'

packages/richtext-lexical/src/features/blocks/server/nodes/InlineBlocksNode.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export type InlineBlockFields<TInlineBlockFields extends JsonObject = JsonObject
2020
id: string
2121
} & TInlineBlockFields
2222

23-
export type SerializedServerInlineBlockNode = Spread<
23+
export type SerializedInlineBlockNode<TBlockFields extends JsonObject = JsonObject> = Spread<
2424
{
2525
children?: never // required so that our typed editor state doesn't automatically add children
26-
fields: InlineBlockFields
26+
fields: InlineBlockFields<TBlockFields>
2727
type: 'inlineBlock'
2828
},
2929
SerializedLexicalNode
@@ -52,7 +52,7 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
5252
return {}
5353
}
5454

55-
static importJSON(serializedNode: SerializedServerInlineBlockNode): ServerInlineBlockNode {
55+
static importJSON(serializedNode: SerializedInlineBlockNode): ServerInlineBlockNode {
5656
const node = $createServerInlineBlockNode(serializedNode.fields)
5757
return node
5858
}
@@ -84,7 +84,7 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
8484
return { element }
8585
}
8686

87-
exportJSON(): SerializedServerInlineBlockNode {
87+
exportJSON(): SerializedInlineBlockNode {
8888
return {
8989
type: 'inlineBlock',
9090
fields: this.getFields(),

packages/richtext-lexical/src/features/blocks/server/validate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { Block } from 'payload'
33
import { fieldSchemasToFormState } from '@payloadcms/ui/forms/fieldSchemasToFormState'
44

55
import type { NodeValidation } from '../../typesServer.js'
6-
import type { SerializedInlineBlockNode } from '../client/nodes/InlineBlocksNode.js'
76
import type { BlockFields, SerializedBlockNode } from './nodes/BlocksNode.js'
7+
import type { SerializedInlineBlockNode } from './nodes/InlineBlocksNode.js'
88

99
export const blockValidationHOC = (
1010
blocks: Block[],

packages/richtext-lexical/src/nodeTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type {
88
} from 'lexical'
99

1010
import type { SerializedQuoteNode } from './features/blockquote/server/index.js'
11-
import type { SerializedInlineBlockNode } from './features/blocks/client/nodes/InlineBlocksNode.js'
1211
import type { SerializedBlockNode } from './features/blocks/server/nodes/BlocksNode.js'
12+
import type { SerializedInlineBlockNode } from './features/blocks/server/nodes/InlineBlocksNode.js'
1313
import type {
1414
SerializedTableCellNode,
1515
SerializedTableNode,

0 commit comments

Comments
 (0)