Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save arkadiyk/f3a5c8df8f3ae7313ca0df862b1d0f93 to your computer and use it in GitHub Desktop.
Save arkadiyk/f3a5c8df8f3ae7313ca0df862b1d0f93 to your computer and use it in GitHub Desktop.
Super simple Rich Text Editor SurfaceUI Component ( Elixir / Phoenix / LiveView )

Super simple Rich Text Editor SurfaceUI Component ( Elixir / Phoenix / LiveView )

I have created a library based on the below snippets: https://github.com/arkadiyk/surface_rich_components

Component

defmodule Components.TextEditor do
  use Surface.Component

  prop name, :string, required: true
  prop value, :string
  prop class, :string

  def render(assigns) do
    ~F"""
    <div :hook="TextEditor" id={"editor-#{@name}"} data-value={@value} data-class={@class}>
      <div data-editor={@name}></div>
      <input type="hidden" name={@name} value={@value} data-editor-hidden={@name}>
    </div>
    """
  end
end

Hook

import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";

const TextEditor = {
  editor: null,
  content: null,
  buttonSetup: {
    bold: { run: (editor) => editor.chain().focus().toggleBold().run(), check: (editor) => editor.isActive("bold") },
    italic: { run: (editor) => editor.chain().focus().toggleItalic().run(), check: (editor) => editor.isActive("italic") },
    h1: {
      run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
      check: (editor) => editor.isActive("heading", { level: 1 }),
    },
  },

  initialValue() {
    return this.el.dataset.value || "";
  },
  classes() {
    return this.el.dataset.class || "";
  },
  mounted() {
    const element = this.el.querySelector("[data-editor]");
    const hidden = this.el.querySelector("[data-editor-hidden]");
    const controlButtons = this.el.closest("form").querySelectorAll("[data-editor-control]");

    controlButtons.forEach((btn) => {
      btn.addEventListener("click", (e) => {
        const config = this.buttonSetup[e.target.dataset.editorControl];
        if (config && typeof config.run === "function") {
          config.run(this.editor);
        }
      });
    });

    this.editor = new Editor({
      element,
      extensions: [StarterKit],
      onUpdate: ({ editor }) => {
        this.content = editor.getHTML();
        hidden.value = this.content;
        hidden.dispatchEvent(new Event("input", { bubbles: true }));
        console.log("content", this.content);
      },
      content: this.initialValue(),
      editorProps: {
        attributes: {
          class: this.classes()
        }
      },
      onTransaction: ({ editor }) => {
        controlButtons.forEach((btn) => {
          const config = this.buttonSetup[btn.dataset.editorControl];
          if (config && typeof config.check === "function") {
            if (config.check(this.editor)) {
              btn.classList.add("editor-active");
            } else {
              btn.classList.remove("editor-active");
            }
          }
        });
      },
    });
  },
};

export { TextEditor };

Usage

defmodule ArticleForm do
  use Surface.LiveComponent

  alias Surface.Components.Form
  alias Components.TextEditor

  data article, :map, default: %{"text" => ""}

  def handle_event("change", a, socket) do
    IO.inspect(a)
    {:noreply, socket}
  end

  def render(assigns) do
    ~F"""
    <div>
      <Form for={:article} change="change" opts={autocomplete: "off"}>
        <button  type="button" data-editor-control="bold">Bold</button>
        <button  type="button" data-editor-control="italic">italic</button>
        <button  type="button" data-editor-control="h1">h1</button>
      
        <TextEditor name="desc" value={@article["text"]} class="sm:prose xl:prose-lg m-5 focus:outline-none"/>
      </Form>
    </div>
    """
  end
end
@mindok
Copy link

mindok commented Apr 18, 2023

Thanks for sharing this! I ended up using non-form based event handlers and standard components rather than Surface, but what you put together here helped me work through it so much quicker than starting from scratch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment