I am trying to integrate a TipTap editor into my edit FormComponent. Right now I have a separate LiveView component for the editor:
@impl true
def render(%{value: %Phoenix.HTML.FormField{} = field} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
~H"""
<div phx-hook="TextEditor" class="tiptap-editor" id={"editor-#{@name}"} phx-update="ignore" data-class={@class} data-value={@value.value}>
<div data-editor={@name}></div>
<.input type="text" name={@name} field={@value} data-editor-hidden={@name} phx-change="validate" />
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
Called from the FormComponent:
<.simple_form
for={@form}
id="project-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:title]} type="text" label="Title" />
<.live_component
module={FeedWeb.ProjectLive.EditorComponent}
id="tiptap-editor"
name="body"
class="w-full p-2 mt-2 border rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 border-zinc-300 focus:border-zinc-400"
value={@form[:body]}
/>
<%!-- <.input field={@form[:body]} type="textarea" rows="12" label="Body" /> --%>
<:actions>
<.button phx-disable-with="Saving..." class="px-2 py-1 text-white bg-blue-500 rounded-md">
Save Project
</.button>
</:actions>
</.simple_form>
And on app.js, some code to hydrate the editor:
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,
Typography,
Table.configure({
resizable: false,
}),
TableCell,
TableHeader,
TableRow,
],
onUpdate: ({ editor }) => {
this.content = editor.getHTML();
hidden.value = this.content;
// this.pushEventTo(this.el, "change", { value: this.content });
// 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");
}
}
});
},
});
},
};
(this would be put into the hooks param)
The problem is when I hit save, whatever typed into the editor is not saved. I’m guessing since I’m wrapping the entire editor component in phx-update=“ignore” the value of the hidden input is not being synced to the server and hence not submitted to the form. I have tried to use pushEventTo on editor change, but somehow that unfocuses the editor on every keystroke.
I’d appreciate it if someone can explain for me how this works and how I should fix it! Would really love to understand how this should work.