Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do you render html without the Editor ? #657

Closed
ThomasKientz opened this issue Apr 8, 2020 · 31 comments
Closed

How do you render html without the Editor ? #657

ThomasKientz opened this issue Apr 8, 2020 · 31 comments
Labels
Type: Bug The issue or pullrequest is related to a bug

Comments

@ThomasKientz
Copy link

Hi,

I don't find a clear way of how to consume the templates produced with the editor.

Once your users have created some nice templates with the editor (eg in a "Edit" page), how do you render them if you just want to display them without any editing functionalities (eg in a "Blog" page in a read only mode) ?

To avoid spin up again a full editor in read only mode (:editable="false"), I am trying to naively create a minimalist renderer component that takes in parameters the compiled template string (produced from getHTML()) and render it with all my custom nodes/components. But compiling templates downloaded from a backend on the fly its not straightforward.

How do you process for this kink of scenery ?

Thanks !

@ThomasKientz ThomasKientz added the Type: Bug The issue or pullrequest is related to a bug label Apr 8, 2020
@BrianHung
Copy link
Contributor

What's the issue with using serialized HTML from getHTML()? As long as you have the same css, I would guess the rendered HTML from that should appear the same although with no editing capabilities.

@ThomasKientz
Copy link
Author

ThomasKientz commented Apr 8, 2020

@BrianHung You mean with v-html ? Cause the serialized html can includes custom components (eg <div><custom-component /></div>). How do you do in this case ?

@ThomasKientz
Copy link
Author

ThomasKientz commented Apr 8, 2020

@BrianHung I think I got your answer wrong.

What's the issue with using serialized HTML from getHTML() ?

There is none.

If you store it in a DB and retrieve it later, how would you render it ?

@BrianHung
Copy link
Contributor

@ThomasKientz

I think I understand your question better now: you want to render the HTML into normal Vue Components. I would look at Editor.js and try to isolate the parts which renders the components, like initNodeViews and DOMParser.fromSchema.

@ThomasKientz
Copy link
Author

ThomasKientz commented Apr 8, 2020

@BrianHung Yes, but I don't understand why I am the first to ask this, I must have missed something.

How do you (tiptap's users) consume the serialized HTML usually ? Editing is one part of the story, you have to consume the content next, don't you ?

@BrianHung
Copy link
Contributor

I just use the editor to render the HTML and use read-only.

@ThomasKientz
Copy link
Author

Ok got it. I don't know why it did not seem intuitive to me to use the editor to only display content at first glance. I am working on a CMS app where most of users are in a "consume only" mode. At first I thought that the serialized HTML was meant to be used with the v-html attribute but I am using custom components. Anyway, I will be using the editor with read-only. Thanks for the clarification.

@mojtabaahn
Copy link

I come here with same goal. to make components both for editing and reading modes.

I would appreciate anyone having experience confirming the approach @ThomasKientz is using.

Thanks

@hanspagel
Copy link
Contributor

That’s what we made the read-only mode for, so it’s fine. 👍️

@jonathanmach
Copy link
Contributor

Hi @ThomasKientz, would you care to share any experience you had with this suggested approach (ie. using the read-only mode)?
I was also trying to find something that would be responsible for rendering the JSON without the editor.

I tried to build a minimal project with TipTap using the read-only mode to see what would be the final bundle size and wasn't surprised to see a 316Kb(parsed), gzipped: 96Kb, since it packs everything required for the actual editor with all its editing functionality.
I feel like I'm after a lighter library that is simply responsible for rendering the JSON content to HTML on the client.
@hanspagel, would love to hear your thoughts on this!

Thanks so much in advance, guys!

Screenshot 2020-11-21 at 19 15 58

@philippkuehn
Copy link
Contributor

There are three ways:

  1. you don't have any nodes with node views: you can simply store and render getHTML()

  2. you have some nodes with complex node views: store the output of getJSON() and render the editor with editable: false

  3. you have some nodes with complex node views and want to use getHTML() to render a string of HTML: provide always a correct toDOM() function. this function is used to generate the HTML and provides a proper copy/paste behavior for every node.

@jonathanmach
Copy link
Contributor

Thanks for your input @philippkuehn!

It seems like the guys at Storyblok created a HTML render for ProseMirror JSON data:
https://github.com/storyblok/storyblok-js-client#method-storyblokrichtextresolverrender

Storyblok.richTextResolver.render(blok.richtext)

What they call richtext is just a ProseMirror JSON state.

And this is the parser logic: https://github.com/storyblok/storyblok-js-client/blob/933dc19369fd6b62db42595781a15b957b663e6a/source/richTextResolver.js#L20

This might be useful for anyone looking into writing their own lightweight renderer.

@philippkuehn
Copy link
Contributor

philippkuehn commented Nov 22, 2020

Hey, in v2 there is also a generateHTML(json, extensions) method to generate HTML without an editor instance (so you can use it on a server).

@jonathanmach
Copy link
Contributor

That's great news, thanks for sharing @philippkuehn.
I'm really excited and looking forward to the new version and hope to be able to contribute in any way 🚀

@ThomasKientz
Copy link
Author

ThomasKientz commented Nov 23, 2020

I ending up exporting html with getHtml() and rendering it with the editor and editable: false.

I am doing this, because I am using custom Vue components as nodes. So I can't render the html as is as I need a way to parse and render the customs nodes (eg <my-custom-component>some content</ my-custom-component>).

A lightweight reader components (to replace editable: false) would be awesome but I have go no time to implement it and I don't care about bundle size.

@Winexcel
Copy link

Winexcel commented Jun 5, 2021

I have similiar issue, i have my custom nodes, for example - spoiler node, which show and hide the content by the button click, this just update attribute "show", and when i toogle editor to read-only mode then click by the button doesn't work, i don't know what i really need to do to fix this problem (#1415) . Maybe do you know?

@multipliedtwice
Copy link

Hey, in v2 there is also a generateHTML(json, extensions) method to generate HTML without an editor instance (so you can use it on a server).

I tried to use it, it says:

ReferenceError: window is not defined
    at doc (C:\server\node_modules\prosemirror-model\src\to_dom.js:194:30)

I also tried to add it to jsdom window, still no result.

@Winexcel @ThomasKientz @philippkuehn
Is it okay to save client-side rendered HTML from editor.view.dom.innerHTML and then serve it for preview after reload?

@hanspagel
Copy link
Contributor

@Winexcel
Copy link

Winexcel commented Jun 22, 2021

Hey, in v2 there is also a generateHTML(json, extensions) method to generate HTML without an editor instance (so you can use it on a server).

I tried to use it, it says:

ReferenceError: window is not defined
    at doc (C:\server\node_modules\prosemirror-model\src\to_dom.js:194:30)

I also tried to add it to jsdom window, still no result.

@Winexcel @ThomasKientz @philippkuehn
Is it okay to save client-side rendered HTML from editor.view.dom.innerHTML and then serve it for preview after reload?

If you use custom extensions that use events, this won't work for you.

For each such extension you need to write a parser that will implement logic on top of html.

Just see this extension https://github.com/Winexcel/tiptap-spoiler
That's what I am talking about.

@JenuelDev
Copy link

There are three ways:

  1. you don't have any nodes with node views: you can simply store and render getHTML()
  2. you have some nodes with complex node views: store the output of getJSON() and render the editor with editable: false
  3. you have some nodes with complex node views and want to use getHTML() to render a string of HTML: provide always a correct toDOM() function. this function is used to generate the HTML and provides a proper copy/paste behavior for every node.

I dont know if any of you notice, with the help of VS Code IDE I was able to find generateHTML function that accepts the json generated data. Since I am using the vue, I can do this, I dont know if this exist on other framework but works on me.

import {  generateHTML } from "@tiptap/vue-3";
const jsonData = {}; // the generated json from the editor
const html = generate(jsonData); // then I just render this on the html

the reason Im json json to the database is becuase of the xss issue.

@gitcatrat
Copy link

There is a fourth option and it's usually recommended by some rich text editors that I've used/evaluated.

Why proposed solutions might not work for you?

  • editable: false is a footgun for many apps - you're probably doubling your bundle for read-only i.e most users (5x in my case) and running the editor for 100s of e.g posts in a list view.. Maybe it uses super light code path but still..
  • it's discouraged to store HTML and inject it dangerously in most rendering frameworks (not to mention complex elements)

Solution: write your own renderer. I didn't say it's the easiest solution. 🤠

// super basic example in JSX
function renderElement(el) {
  const children = el.content.map(child => renderElement(child));

  // few handled cases to give a taste
  if (el.type === 'text') return el.text;
  if (el.type === 'paragraph') return <p>{children}</p>;

  return null;
}

function Document(props) {
  return (
    <div>
     {/* props.document === {type: "doc", content: [...]} */}
     {props.document.content.map(child => renderElement(child))}
   </div>
  );
}

Few points to make:

  • sorry folks, don't have working solution to give you, still evaluating tiptap and messing around
  • use the same CSS classes for editor elements and renderer to get identical appearance
  • more complex nodes probably have to be rewritten in your framework of choice (or share logic with interactive node)
  • marks is a bit pain in the bum with tiptap JSON structure, you need to lift them up to generate decent HTML
    see: Any good ideas how to render marks correctly? #3377
  • I just started writing mine (done it for other editors), so it's possible that the issues don't end there

@cluah
Copy link

cluah commented Mar 3, 2023

Hi. So far there is no way to do it? Thanks.

@michelson
Copy link

we did that on Dante3, (tiptap based editor) it let you render custom components (react)
https://github.com/michelson/Dante/blob/main/packages/dante3/src/renderer.tsx
demo at: https://www.dante-editor.dev/posts/renderer

@bkyerv
Copy link

bkyerv commented May 25, 2023

I think one of the reasons a built in renderer has not been built by the team behind tiptap is that a renderer that would cover all edge cases in the limit will be the editor component. Therefore I guess if we need any type of lighter version of the rendered we will have to implement it ourselves depending on the case at hand

@BrianHung
Copy link
Contributor

Would look into https://github.com/nytimes/react-prosemirror

@michelson
Copy link

here is a renderer implemented in ruby https://github.com/chaskiq/chaskiq/blob/main/app/services/dante/renderer.rb This is useful to render HTML from ruby backends.
you could probably translate that to any language with chat-gpt

@abdullahmehboob20s
Copy link

you can do

const content : "<p>Some text here</p>";

return (
   <div dangerouslySetInnerHTML={{ __html: content }}  />
)

@LuizPelegrini
Copy link

would love to know whether there's some kind of block renderer built for tiptap, much like what we have for Editorjs here and here

@seeARMS
Copy link

seeARMS commented Sep 8, 2024

Just adding a note that I would also love to use some type of renderer. The less-than-ideal approach I'm taking so far:

  • store the generated HTML alongside the JSON
  • on public page load, render the HTML (so it's immediately available on page load)
  • dynamically load the TipTap editor in readonly mode
  • when TipTap is initialized, replace the statically rendered HTML with the tiptap editor in readonly mode

The downsides of this approach:

  • the rendered HTML is not always exactly what we want. For example, I want to use next/image to optimize images, but it doesn't make sense to store the next/image HTML tag; so we store the img tag, but that causes the full-size unoptimized image to be fetched on page load before loading the optimized image
  • this of course bundles all TipTap extensions and ships them to the client, which is not ideal. Some extensions (eg the emoji one, or the LaTeX one) are large, but not many posts we're displaying use them, so we're shipping them needlessly

Some ideas I've toyed around with to improve this:

  • create a custom renderer, as explained above. In particular this solution looks what I had in mind
  • traverse the tiptap JSON for a post and dynamically import only the extensions used in any given post. I'm hopeful this would not bundle any unneeded extensions

EDIT: It actually seems easier than expected to create the custom renderer, and it gives us more flexibility, so we'll be proceeding with that.

@jack-szeto
Copy link

I found a way to render HTML from the TipTap editor without worrying about style collisions. I used an iframe to keep things isolated. Here's the code:

const SimpleHtmlRenderer = ({ html }: { html: string }) => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  useEffect(() => {
    const iframe = iframeRef.current;
    if (iframe) {
      iframe.onload = () => {
        iframe.style.height =
          iframe.contentWindow?.document.body.scrollHeight + 'px';
      };
    }
  }, [html]);
  return (
    <iframe
      ref={iframeRef}
      srcDoc={html}
      style={{ border: 'none', width: '100%' }}
      sandbox="allow-same-origin"
    />
  );
};

This way, I don't have to worry about any style collisions. The iframe's height adjusts automatically based on its content, so it fits nicely.

Hope this helps!

@nperez0111
Copy link
Contributor

This will be the official supported solution: #5528

It is usable now by installing the @tiptap/static-renderer package commented by the pkg-pr-new bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Bug The issue or pullrequest is related to a bug
Projects
None yet
Development

No branches or pull requests