atelierdisko / payload-lexical-react-renderer

MIT License
31 stars 5 forks source link

trying out your renderer with Lexical playground elements throws an error #6

Open dohomi opened 4 months ago

dohomi commented 4 months ago

I am trying out your renderer component with the Lexical playground elements like youtube, image, separator. The serialize callback function throws an error

TypeError: Cannot read properties of undefined (reading 'map')

I think the reason behind is, that there are custom elements which do not have node.children set so I would think that the custom block types should be rendered through the blockRenderes but they are rendered as renderElements. This is how a JSON of Youtube for example looks like:

      {
        format: "",
        type: "youtube",
        version: 1,
        id: "kBUAV4YT1fs",
        loop: false,
        controls: true,
        aspectRatio: "4/3",
        autoplay: false,
        provider: "youtube"
      },
schaschjan commented 4 months ago

@dohomi We did not encounter this issue as the configuration of the editor inside payload does not allow to add these elements (I think). Could you provide me the full editor content state so I can reproduce this issue 🙏

dohomi commented 4 months ago

hey @schaschjan this is how my editor JSON looks like:

export const lexicalPreJson = {
  root: {
    children: [
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Hello World",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "heading",
        version: 1,
        tag: "h1"
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Hello World",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "right",
        indent: 0,
        type: "heading",
        version: 1,
        tag: "h2"
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "this is a simple ",
            type: "text",
            version: 1
          },
          {
            detail: 0,
            format: 1,
            mode: "normal",
            style: "",
            text: "hello",
            type: "text",
            version: 1
          },
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: " ",
            type: "text",
            version: 1
          },
          {
            detail: 0,
            format: 10,
            mode: "normal",
            style: "",
            text: "world",
            type: "text",
            version: 1
          },
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: " example!!",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Ordered list:",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "first",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 1
          },
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "second",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 2
          },
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "third",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 3
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "list",
        version: 1,
        listType: "number",
        start: 1,
        tag: "ol"
      },
      {
        children: [],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Bullet list:",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "first",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 1
          },
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "second",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 2
          },
          {
            children: [
              {
                detail: 0,
                format: 0,
                mode: "normal",
                style: "",
                text: "third",
                type: "text",
                version: 1
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "listitem",
            version: 1,
            value: 3
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "list",
        version: 1,
        listType: "bullet",
        start: 1,
        tag: "ul"
      },
      {
        children: [],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Some blockquote??",
            type: "text",
            version: 1
          },
          {
            type: "linebreak",
            version: 1
          },
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "isnt that great?",
            type: "text",
            version: 1
          },
          {
            type: "linebreak",
            version: 1
          },
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Here comes a blockquote statement",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "quote",
        version: 1
      },
      {
        children: [
          {
            type: "linebreak",
            version: 1
          },
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "Left align",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "center align",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "center",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: "normal",
            style: "",
            text: "right align",
            type: "text",
            version: 1
          }
        ],
        direction: "ltr",
        format: "right",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [],
        direction: null,
        format: "right",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [],
        direction: null,
        format: "left",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        format: "",
        type: "youtube",
        version: 1,
        id: "kBUAV4YT1fs",
        loop: false,
        controls: true,
        aspectRatio: "4/3",
        autoplay: false,
        provider: "youtube"
      },
      {
        children: [
          {
            altText: "",
            caption: {
              editorState: {
                root: {
                  children: [],
                  direction: null,
                  format: "",
                  indent: 0,
                  type: "root",
                  version: 1
                }
              }
            },
            height: 0,
            position: "full",
            showCaption: false,
            src: "https://placehold.co/600x400/png",
            type: "inline-image",
            version: 1,
            width: 0
          }
        ],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        type: "horizontalrule",
        version: 1
      },
      {
        children: [],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      },
      {
        children: [
          {
            children: [
              {
                children: [
                  {
                    detail: 0,
                    format: 0,
                    mode: "normal",
                    style: "",
                    text: "This is left column",
                    type: "text",
                    version: 1
                  }
                ],
                direction: "ltr",
                format: "",
                indent: 0,
                type: "paragraph",
                version: 1,
                textFormat: 0
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "layout-item",
            version: 1
          },
          {
            children: [
              {
                children: [
                  {
                    detail: 0,
                    format: 0,
                    mode: "normal",
                    style: "",
                    text: "this is right column",
                    type: "text",
                    version: 1
                  }
                ],
                direction: "ltr",
                format: "",
                indent: 0,
                type: "paragraph",
                version: 1,
                textFormat: 0
              }
            ],
            direction: "ltr",
            format: "",
            indent: 0,
            type: "layout-item",
            version: 1
          }
        ],
        direction: "ltr",
        format: "",
        indent: 0,
        type: "layout-container",
        version: 1,
        templateColumns: "1fr 1fr"
      },
      {
        children: [],
        direction: null,
        format: "",
        indent: 0,
        type: "paragraph",
        version: 1,
        textFormat: 0
      }
    ],
    direction: "ltr",
    format: "",
    indent: 0,
    type: "root",
    version: 1
  }
}

The Lexical editor works as intended, only the renderer doesn't work due to the .childen of undefined error

dohomi commented 4 months ago

Looking at the source code of PayloadLexicalReactRenderer it seems that the custom block from the Lexical playground such as youtube layout-container layout-item are not of type block, so the property blockRenderers is not applying to it. I am not sure if I do anything wrong in my Lexical config but I was just using some custom components from the Lexical playground. I fixed it now with following lines of code, even though the types are not yet 100% ideal:

  const serialize = React.useCallback(
(...)
        if (node.type === "block") {
          const renderer = blockRenderers[node.fields.blockType] as (
            props: unknown
          ) => React.ReactNode

          if (typeof renderer !== "function") {
            throw new Error(`Missing block renderer for block type '${node.fields.blockType}'`)
          }

          return <React.Fragment key={index}>{renderer(node)}</React.Fragment>
        }
        if (node.type) {
          const renderer = blockRenderers[node.type]
          if (typeof renderer === "function") {
            return (
              <React.Fragment key={index}>
                {(renderer as Function)({
                  ...node,
                  ...("children" in node && { children: serialize(node.children) })
                })}
              </React.Fragment>
            )
          }
        }
(...)

This way all provided blockRenderers are now rendering if the node.type is matching.

janus-reith commented 4 months ago

I might be wrong, but think this should not be covered by blockRenderers, since these blocks are solely a payload concept and not "native" to Lexical. The ones like "type": "youtube" are still elements, just like "type": "block" is.

That being said, features like a youtube video could aswell be implemented as a payload block and have a custom renderer within the editor so using them could feel the same way. Perhaps a bit OT: I feel like there might be room for improvement within payload or lexical to align the payload-introduced blocks and adding custom element renderers to Lexical in a better way. Using youtube as an example: Maybe implementing the actual youtube renderer as native Lexical component, and then specific to payload, just a small wrapper that would take care of the data layer, so e.g. passing the youtube video url from a payload source. At best, it wouldn't even be necessary to create a specific payload wrapper per element, but a generic way to deduce supported props for custom Lexical element types. Not sure how feasible this is right now since I believe Lexical elements don't export a specific interface to describe input fields they support.

janus-reith commented 4 months ago

The actual fix here that seems more correct to me would be to allow extending type ElementRenderers and replacing the current if (node.type === "xyz") checks in renderElement with a more dynamic approach that supports arbitrary node types.