I have created a library based on the below snippets: https://github.com/arkadiyk/surface_rich_components
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
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 };
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
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.