How should I load 'large' data on client (Tiptap editor)?

I’m trying to run the Tiptap editor in a Liveview using via app.js’s Hooks. The way to pass data in this case is via <... data-content={mycontent}>. But this will balloon when the document grows.

First: is going through Hooks on app.js a good strategy? Is there a better way to approach it?

I asked ChatGPT for an assist and it offered both a fetch call or a native Phoenix handle-event. I went the Phoenix route.

I’ve tried all I could with ‘handle-event’, but the ‘handle-event’ simply does not fire.

This is a portion of the app.js file:

... // load tiptap libraries

let Hooks = {};

Hooks.tiptapEditor = {
  mounted() {
    this.handleEvent("load-content", ({content}) => {
      console.log("handleEvent content", content);
      json_content = JSON.parse(content);
      this.editor.commands.setContent(json_content);
    });
    this.editor = new Editor({
      element: this.el,
      // content: "<p>Hello, world.</p>",
      editorProps: {
        attributes: {
          class:
            "min-h-[360px]",
        },
      },
      extensions: [StarterKit],
      onCreate: ({ editor }) => {
        this.pushEvent("editor-ready");
        editor.commands.focus("end");
      },
    });
...
}

and here’s my show.ex

defmodule TiptapExWeb.DocumentLive.Show do
  use TiptapExWeb, :live_view

  alias TiptapEx.Documents

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  @impl true
  def handle_params(%{"id" => id}, _, socket) do
    document = Documents.get_document!(id)
    # send_tiptap_content(socket, document)

    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
     |> assign(:document, document)}
  end

  @impl true
  def handle_event("save-document", params, socket) do
    document = socket.assigns.document
    {:ok, updated_document} = Documents.update_document(document, params)

    {:noreply,
     socket
     |> assign(:document, updated_document)}
  end

  def handle_event("editor-ready", _, socket) do
    send_tiptap_content(socket, socket.assigns.document)
  end

  def send_tiptap_content(socket, document) do
    push_event(socket, "load-content", %{content: document.json})
    {:noreply, assign(socket, document: document)}
  end

  defp page_title(:show), do: "Show Document"
  defp page_title(:edit), do: "Edit Document"
end

Here’s the editor running with the console open. It fires the mount and the debouncedSave function, but not any of the others.

Any ideas?

It’s probably that this in JavaScript has its own scope in an arrow function. When you call this.pushEvent it’s not from the this from the hook. See this - JavaScript | MDN

In the mount function try to something like const hook = this. Then call it with hook.pushEvent.

That’s a good hunch, but it wasn’t it.

I finally found the answer thanks to: Use case for returning {:reply, map, socket} in handle_event callback - #2 by cmo

The relevant code

  def handle_event("load-document", _, socket) do
    {:reply, %{json: socket.assigns.document.json}, socket}
  end

in the app.js hook

  this.pushEvent("load-document", {}, 
    ({json}) => {
      this.editor.commands.setContent(json, true);
    });
1 Like

Related to this, I was breaking my head about how to load different extensions and I guess I can do the following:

Hooks.tiptapEditor = {
  mounted() {
    this.pushEvent("load-settings", {}, ({ content, extensions }) => {
      this.editor = new Editor({
        ...
        content: content,
        extensions: extensions
        ...
        });
    })
  }