Skip to content

Commit 80ae745

Browse files
authored
docs: adds code examples for how to fully implement textStateFeature (#16053)
Adds a "Rendering on the frontend" section under `TextStateFeature` with clear examples: - a shared config file - how to reference that shared config when setting up TextStateFeature in your field - custom text JSX converter
1 parent d5fe0ce commit 80ae745

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

docs/rich-text/official-features.mdx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,121 @@ This is what the example above will look like:
478478
srcLight="https://payloadcms.com/images/docs/text-state-feature.png"
479479
alt="Example usage in light and dark mode for TextStateFeature with defaultColors and some custom styles"
480480
/>
481+
482+
#### Rendering on the frontend
483+
484+
When Lexical serializes a text node that has state applied, the state is stored under a `"$"` key in the node object. To apply the styles when rendering rich text on your frontend, you need a custom `text` JSX converter that reads from this key and maps the values back to your CSS config.
485+
486+
**Step 1 — Share your state config**
487+
488+
Extract your `TextStateFeature` state into a file with no package imports so it can be safely imported in both server and client contexts:
489+
490+
```ts
491+
// src/fields/textStateConfig.ts
492+
493+
export const textStateConfig = {
494+
color: {
495+
'text-red': {
496+
label: 'Red',
497+
css: {
498+
color:
499+
'light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216))',
500+
},
501+
},
502+
'text-blue': {
503+
label: 'Blue',
504+
css: {
505+
color:
506+
'light-dark(oklch(0.546 0.245 262.881), oklch(0.707 0.165 254.624))',
507+
},
508+
},
509+
// ...other colors
510+
},
511+
} as const
512+
```
513+
514+
Then reference it in your field config:
515+
516+
```ts
517+
// src/blocks/Content/config.ts
518+
519+
import { TextStateFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
520+
import { textStateConfig } from '@/fields/textStateConfig'
521+
522+
{
523+
name: 'richText',
524+
type: 'richText',
525+
editor: lexicalEditor({
526+
features: ({ rootFeatures }) => [
527+
...rootFeatures,
528+
TextStateFeature({
529+
state: {
530+
color: textStateConfig.color,
531+
},
532+
}),
533+
],
534+
}),
535+
}
536+
```
537+
538+
**Step 2 — Add a custom text converter**
539+
540+
When using [`RichText` from `@payloadcms/richtext-lexical/react`](/docs/rich-text/lexical#rendering-lexical-content-in-react), override the default `text` converter to read the `"$"` key and apply the corresponding CSS as inline styles:
541+
542+
```tsx
543+
// src/components/RichText/index.tsx
544+
545+
import { textStateConfig } from '@/fields/textStateConfig'
546+
import {
547+
JSXConvertersFunction,
548+
RichText as ConvertRichText,
549+
} from '@payloadcms/richtext-lexical/react'
550+
551+
// Lexical serializes node state under the "$" key.
552+
const NODE_STATE_KEY = '$'
553+
554+
// React's style prop requires camelCase, but TextStateFeature CSS uses hyphen-case.
555+
function hyphenToCamel(str: string): string {
556+
return str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
557+
}
558+
559+
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
560+
...defaultConverters,
561+
text: (args) => {
562+
const { node } = args
563+
564+
// Render standard formatting (bold, italic, etc.) using the default converter
565+
let text =
566+
typeof defaultConverters.text === 'function'
567+
? defaultConverters.text(args)
568+
: node.text
569+
570+
// Apply TextStateFeature styles from the "$" key in the serialized node
571+
const nodeState = (node as any)[NODE_STATE_KEY] as
572+
| Record<string, string>
573+
| undefined
574+
if (nodeState) {
575+
const styles: React.CSSProperties = {}
576+
for (const [stateKey, stateValue] of Object.entries(nodeState)) {
577+
const css = (textStateConfig as any)[stateKey]?.[stateValue]?.css
578+
if (css) {
579+
for (const [prop, value] of Object.entries(css)) {
580+
;(styles as any)[hyphenToCamel(prop)] = value
581+
}
582+
}
583+
}
584+
if (Object.keys(styles).length > 0) {
585+
text = <span style={styles}>{text}</span>
586+
}
587+
}
588+
589+
return text
590+
},
591+
})
592+
593+
export default function RichText({ data, ...rest }) {
594+
return <ConvertRichText converters={jsxConverters} data={data} {...rest} />
595+
}
596+
```
597+
598+
The key insight is that `textStateConfig` is the single source of truth — it is passed directly to `TextStateFeature` in your field config and also imported by your frontend converter to resolve the CSS values at render time.

0 commit comments

Comments
 (0)