Storing JSON in Phoenix using Rich Text Editor

Most Rich Text Editors like Prosemirror, TipTap give JSON output these days. If we want to store the JSON in Ecto database how do we do that?

I have a content field defined as content, :map, default: %{} in the ecto schema. Now, in the form I have <div id="summary" phx-hook="Prosemirror" />. The Prosemirror hook will create the instance of editor. I registered an event listener for the editor - and - onChange - I get the content using editor.getJSON. How do I store this in the field content?
I tried keeping a hidden field hidden_input(f, :content) and assigned the value of getJSON to value of the above field. It is not working. It says HTML.Safe() is not implemented for Map.
How to solve this problem?

I wouldn’t think to store it in an <input>. Sounds like it is part of a larger form, or is it the only field you’re editing? The latter will be easier to handle.

You can send it from the Hook to the liveview/livecomponent using pushEvent or pushEventTo.

1 Like

In the hook, something like this?

    editor.on("update", ({ editor }) => {
      this.pushEvent("summary-updated", { content: editor.getJSON });
    });

And then in the live_component

  def handle_event("summary-updated", content, socket) do
    {:noreply, assign(socket, content: content)}
  end

finally in the form -

<div id="summary-tiptap" phx-hook="Tiptap" phx-update="ignore" class="tiptapeditor" />
<%= hidden_input(f, :summary, value: Jason.encode!(@content)) %>

this is what I have. It is still not working. Any further clues?

I’m not sure why you’re trying to put a json blob in a hidden input. Are there other inputs in this form?

There are title and summary fields. So, another text field for title is there. I have to put the value of the JSON from the rich text editor into the summary - and - I thought changeset will work on the insertion.
JS-based rich text editor disappers as soon as page loaded - #2 by gdub01 is what prompted me.

If you have content in your assigns, do you need to put it in an input so you’ll get it back in your params?

1 Like

I have done something similar some time ago… before liveview, with react/draft.js

It looked like this…

import React, {useState} from "react";

import MyEditor from "../my_editor";

const DraftInput = (props) => {
  // src is an encoded json string or null
  const {src, name} = props;
  const jsonOrNull = src ? JSON.parse(src) : null;

  const [jsonString, setJsonString] = useState("null");
  const handleOnChange = data => setJsonString(JSON.stringify(data));

  return (
    <>
      <input type="hidden" value={jsonString} name={name} />
      <MyEditor src={jsonOrNull} handleOnChange={handleOnChange} />
    </>
  )
}

export default DraftInput;

More or less similar approach, with JSON.parse and JSON.stringify.

1 Like

I am really confused - this handle_event is not getting called. The first hook is in the app.js file and I am getting an error saying handle_event/3 is undefined or private.
Struggling for a few hours but not finding what I am missing. :frowning:

The way I deal with this is to add a catch all clause…

def handle_event(message, params, socket) do
  Logger.info("#{message} #{inspect params}")
  {:noreply, socket}
end

Also…

…I would use instead

def handle_event("summary-updated", %{"content" => content}, socket) do
1 Like

Just a side question @koklegorille - If you have to implement this again, would you go with the handle_event route or hidden input route?

Nowadays I would go the web component way…

Because it could cleanly encapsulate the editor to something like this…

<my-editor value={@json_string}></my-editor>

What is inside could be javascript, or web assembly. You can leverage attribute change, like in this video.

And it is portable everywhere. Even in non liveview page, because it’s just html.

1 Like
let Tiptap = {
  mounted() {
    new Editor({
      element: this.el,
      extensions: [StarterKit],
      content: "<p>Hello World!</p>",
      editorProps: {
        attributes: {
          class: "prose prose-sm sm:prose lg:prose-lg m-5 focus:outline-none",
        },
      },
      onUpdate({ editor }) {
        this.pushEvent("summary", editor.getJSON);
      },
    });
  },
};

This hook is working. But, onUpdate() is not getting executed. Am I getting into any es6 modules, imports problem here?

One question - @kokolegorille - The data type of field in database is map or text?

I use map in the db, and use Jason to de/serialize data.

1 Like

hey @cvkmohan
my team currently trying to implement prosemirror/remirror into frontend react app and for the same I have to handle that data and then store it in phoenix backend
so I was checking for some resources I came across your post

how you handled prosemirror data stream in your project?
did you use Phoenix Channels?
any information would be a great help.

Hello @ambareesha7 !
Yes, I am in pursuit of implementing Rich Text Editor. I am assuming you are also developing a primarily LiveView application with the need for Rich Text Editor for Text Content.
I wanted to document the entire process with resources as a blog post so that it can help others - your question hastened the process. I will put things up here itself.

  1. Milkdown
  2. ProseMirror
  3. Headless WYSIWYG Text Editor – Tiptap Editor
    These are the three Rich Text Editors that I have explored. Incidentally, all of them have their base as Prosemirror.
  4. ianstormtaylor/slate: A completely customizable framework for building rich text editors. (Currently in beta.) (github.com)
  5. Lexical · An extensible text editor framework that does things differently
    These are two other libraries that I have explored but, discarded them early. Both of them have a strong coupling with React - Lexical is very recent. I was looking more in terms of plain Javascript integration - avoiding JS Framework if possible is one of my objectives.
    Out of these, I discarded Milkdown first. Mainly because, as much as I could understand the documentation, Milkdown allows only one instance on a page. I would be needing multiple instances with various levels of Rich Text Editing. One instance might need a full blown Rich Text Editing, and another will need simple Github flavoured Markdown.
    I explored and integrated both TipTap and ProseMirror into my code using LiveView Hooks. The implementation has been fairly straight forward. As you might have observed even I was into dual mind on whether to store the resultant data as a map or text. Initially, I went with Text format - and - returned the getHTML() of the instance in the updated() event using the hook. My point of view was, by storing the HTML, I am safer even if I have to change the editor in future. Both Prosemirror and TipTap provide HTML export of the content via an event - and - integrating it has been straight forward.
    As my understanding grew, I took a few more decisions.
    a. Though Tiptap provides a lot of extensions, and provides a reasonable compatibility to Prosemirror extensions, I felt it is better to stick to Prosemirror. That allowed much greater flexibility in terms of extensibility. ( Just personal experience. No Value Judgement.)
    b. Instead of storing as RichText, defining a schema and storing the content as a map, provided a lot more control over validation of the Rich Content that I needed in my application. So, I moved from Text data type to Map Data Type for the content.
    c. In the initial stages, the implementation is via hidden field. One hidden field for the content - One div with a hook to instantiate the RichText Instance. It worked reasonably well. Prosemirror example: replacing form textareas (github.com) This gist has been useful in that implementation. Very useful.
    d. However, the above implementation does not help in fine grained validation of the content - specially for the sort of validation my application needed. That is when I came across this library - Omerlo-Technologies/ex_prosemirror: ExProsemirror is a toolkit that integrates the ProseMirror rich-text editor into elixir/phoenix. (github.com) - This module integrates prosemirror into phoenix with a very different approach. Though, in its early stages of implementation, the idea this module took up really caught my attention.
    At present, I am extending the idea of exprosemirror and writing my own library for the custom validations and other stuff my application needs.
    I did explore integrating Remirror into the application - and - it worked - thanks mainly due to this article Stephen Bussey - React in LiveView: How and Why?
    Well, this is the brief run through of my experiments. Hope you will find this useful. Feel free to revert back with any questions.
    Edit 1:
    Our own Livebook.dev also has a very interesting take on Rich Text Editing. It gets one level deeper and uses remark - markdown processor powered by plugins to transform markdown. I initially wanted to copy this approach - mainly - because you get even collaborative editing also just by copy-pasting the code from the greats like @chrismccord and @josevalim. However, this approach is more suited if we are looking for more free form rich text. My application needs more control over the rich text - and - specially I would be needing mentions embeds etc in my application.
4 Likes

thanks a lot this info will help me a lot

we are using Next.js in frontend

I was curious to learn more about handling prosemirror sending live changes to backend and store them?

I was experimenting with Delta, quill/delta but they are not suiting my requirements so again looking into remirror stuff

1 Like

Kinda related – but… how did you install TipTap? (or other rich text editors?)

I have some issue running mix assets.deploy, but in development, it works fine because I can just npm install it!

Could not resolve “@tiptap/core”

Also kind of related – there was a topic recently discussing integrating Trix editor with Liveview along with some code snippets that you might helpful if you are unable to get Tiptap working correctly.