Alternative solution to https://github.com/payloadcms/payload/pull/11104. Big thanks to @andershermansen and @GermanJablo for kickstarting work on a solution and bringing this to our attention. This PR copies over the live-preview test suite example from his PR. Fixes https://github.com/payloadcms/payload/issues/5285, https://github.com/payloadcms/payload/issues/6071 and https://github.com/payloadcms/payload/issues/8277. Potentially fixes #11801 This PR completely gets rid of our client-side live preview field traversal + population and all logic related to it, and instead lets the findByID endpoint handle it. The data sent through the live preview message event is now passed to findByID via the newly added `data` attribute. The findByID endpoint will then use this data and run hooks on it (which run population), instead of fetching the data from the database. This new API basically behaves like a `/api/populate?data=` endpoint, with the benefit that it runs all the hooks. Another use-case for it will be rendering lexical data. Sometimes you may only have unpopulated data available. This functionality allows you to then populate the lexical portion of it on-the-fly, so that you can properly render it to JSX while displaying images. ## Benefits - a lot less code to maintain. No duplicative population logic - much faster - one single API request instead of one request per relationship to populate - all payload features are now correctly supported (population and hooks) - since hooks are now running for client-side live preview, this means the `lexicalHTML` field is now supported! This was a long-running issue - this fixes a lot of population inconsistencies that we previously did not know of. For example, it previously populated lexical and slate relationships even if the data was saved in an incorrect format ## [Method Override (POST)](https://payloadcms.com/docs/rest-api/overview#using-method-override-post) change The population request to the findByID endpoint is sent as a post request, so that we can pass through the `data` without having to squeeze it into the url params. To do that, it uses the `X-Payload-HTTP-Method-Override` header. Previously, this functionality still expected the data to be sent through as URL search params - just passed to the body instead of the URL. In this PR, I made it possible to pass it as JSON instead. This means: - the receiving endpoint will receive the data under `req.data` and is not able to read it from the search params - this means existing endpoints won't support this functionality unless they also attempt to read from req.data. - for the purpose of this PR, the findByID endpoint was modified to support this behavior. This functionality is documented as it can be useful for user-defined endpoints as well. Passing data as json has the following benefits: - it's more performant - no need to serialize and deserialize data to search params via `qs-esm`. This is especially important here, as we are passing large amounts of json data - the current implementation was serializing the data incorrectly, leading to incorrect data within nested lexical nodes **Note for people passing their own live preview `requestHandler`:** instead of sending a GET request to populate documents, you will now need to send a POST request to the findByID endpoint and pass additional headers. Additionally, you will need to send through the arguments as JSON instead of search params and include `data` as an argument. Here is the updated defaultRequestHandler for reference: ```ts const defaultRequestHandler: CollectionPopulationRequestHandler = ({ apiPath, data, endpoint, serverURL, }) => { const url = `${serverURL}${apiPath}/${endpoint}` return fetch(url, { body: JSON.stringify(data), credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-Payload-HTTP-Method-Override': 'GET', }, method: 'POST', }) } ``` --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211124793355068 - https://app.asana.com/0/0/1211124793355066
246 lines
7.4 KiB
Plaintext
246 lines
7.4 KiB
Plaintext
---
|
|
title: Converting HTML
|
|
label: Converting HTML
|
|
order: 22
|
|
desc: Converting between lexical richtext and HTML
|
|
keywords: lexical, richtext, html
|
|
---
|
|
|
|
## Rich Text to HTML
|
|
|
|
There are two main approaches to convert your Lexical-based rich text to HTML:
|
|
|
|
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
|
|
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API.
|
|
|
|
### On-demand
|
|
|
|
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
|
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
|
|
|
|
import React from 'react'
|
|
|
|
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
|
const html = convertLexicalToHTML({ data })
|
|
|
|
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
}
|
|
```
|
|
|
|
#### Dynamic Population (Advanced)
|
|
|
|
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
|
|
|
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
|
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
|
import React, { useEffect, useState } from 'react'
|
|
|
|
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
|
const [html, setHTML] = useState<null | string>(null)
|
|
useEffect(() => {
|
|
async function convert() {
|
|
const html = await convertLexicalToHTMLAsync({
|
|
data,
|
|
populate: getRestPopulateFn({
|
|
apiURL: `http://localhost:3000/api`,
|
|
}),
|
|
})
|
|
setHTML(html)
|
|
}
|
|
|
|
void convert()
|
|
}, [data])
|
|
|
|
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
}
|
|
```
|
|
|
|
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
|
|
|
|
```tsx
|
|
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
|
|
|
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
|
|
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
|
import { getPayload } from 'payload'
|
|
import React from 'react'
|
|
|
|
import config from '../../config.js'
|
|
|
|
export const MyRSCComponent = async ({
|
|
data,
|
|
}: {
|
|
data: SerializedEditorState
|
|
}) => {
|
|
const payload = await getPayload({
|
|
config,
|
|
})
|
|
|
|
const html = await convertLexicalToHTMLAsync({
|
|
data,
|
|
populate: await getPayloadPopulateFn({
|
|
currentDepth: 0,
|
|
depth: 1,
|
|
payload,
|
|
}),
|
|
})
|
|
|
|
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
}
|
|
```
|
|
|
|
### HTML field
|
|
|
|
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format.
|
|
|
|
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
|
|
|
|
```ts
|
|
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
|
|
import type { MyTextBlock } from '@/payload-types.js'
|
|
import type { CollectionConfig } from 'payload'
|
|
|
|
import {
|
|
BlocksFeature,
|
|
type DefaultNodeTypes,
|
|
lexicalEditor,
|
|
lexicalHTMLField,
|
|
type SerializedBlockNode,
|
|
} from '@payloadcms/richtext-lexical'
|
|
|
|
const Pages: CollectionConfig = {
|
|
slug: 'pages',
|
|
fields: [
|
|
{
|
|
name: 'nameOfYourRichTextField',
|
|
type: 'richText',
|
|
editor: lexicalEditor(),
|
|
},
|
|
lexicalHTMLField({
|
|
htmlFieldName: 'nameOfYourRichTextField_html',
|
|
lexicalFieldName: 'nameOfYourRichTextField',
|
|
}),
|
|
{
|
|
name: 'customRichText',
|
|
type: 'richText',
|
|
editor: lexicalEditor({
|
|
features: ({ defaultFeatures }) => [
|
|
...defaultFeatures,
|
|
BlocksFeature({
|
|
blocks: [
|
|
{
|
|
interfaceName: 'MyTextBlock',
|
|
slug: 'myTextBlock',
|
|
fields: [
|
|
{
|
|
name: 'text',
|
|
type: 'text',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
},
|
|
lexicalHTMLField({
|
|
htmlFieldName: 'customRichText_html',
|
|
lexicalFieldName: 'customRichText',
|
|
// can pass in additional converters or override default ones
|
|
converters: (({ defaultConverters }) => ({
|
|
...defaultConverters,
|
|
blocks: {
|
|
myTextBlock: ({ node, providedCSSString }) =>
|
|
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
|
},
|
|
})) as HTMLConvertersFunction<
|
|
DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
|
|
>,
|
|
}),
|
|
],
|
|
}
|
|
```
|
|
|
|
## Blocks to HTML
|
|
|
|
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
|
import type {
|
|
DefaultNodeTypes,
|
|
SerializedBlockNode,
|
|
SerializedInlineBlockNode,
|
|
} from '@payloadcms/richtext-lexical'
|
|
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
|
|
|
import {
|
|
convertLexicalToHTML,
|
|
type HTMLConvertersFunction,
|
|
} from '@payloadcms/richtext-lexical/html'
|
|
import React from 'react'
|
|
|
|
type NodeTypes =
|
|
| DefaultNodeTypes
|
|
| SerializedBlockNode<MyTextBlock>
|
|
| SerializedInlineBlockNode<MyInlineBlock>
|
|
|
|
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
|
|
defaultConverters,
|
|
}) => ({
|
|
...defaultConverters,
|
|
blocks: {
|
|
// Each key should match your block's slug
|
|
myTextBlock: ({ node, providedCSSString }) =>
|
|
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
|
},
|
|
inlineBlocks: {
|
|
// Each key should match your inline block's slug
|
|
myInlineBlock: ({ node, providedStyleTag }) =>
|
|
`<span${providedStyleTag}>${node.fields.text}</span$>`,
|
|
},
|
|
})
|
|
|
|
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
|
const html = convertLexicalToHTML({
|
|
converters: htmlConverters,
|
|
data,
|
|
})
|
|
|
|
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
}
|
|
```
|
|
|
|
## HTML to Richtext
|
|
|
|
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):
|
|
|
|
```ts
|
|
import {
|
|
convertHTMLToLexical,
|
|
editorConfigFactory,
|
|
} from '@payloadcms/richtext-lexical'
|
|
// Make sure you have jsdom and @types/jsdom installed
|
|
import { JSDOM } from 'jsdom'
|
|
|
|
const html = convertHTMLToLexical({
|
|
editorConfig: await editorConfigFactory.default({
|
|
config, // Your Payload Config
|
|
}),
|
|
html: '<p>text</p>',
|
|
JSDOM, // Pass in the JSDOM import; it's not bundled to keep package size small
|
|
})
|
|
```
|