Hi, Thank you for helping out!
I created the rich editor myself, the problem is not necessarily about the editor, I should’ve been clearer about that. The issue is that I have a modal with a form inside a live component and that live component lives in another form. I can’t really lift the modal portion out. That’s why I tried moving or portalling or teleporting the modal to the body element but this caused issues with live view updates.
I could be wrong, but I feel like this is a fundamental framework issue. Other frameworks and even Alpine.js support simple portal or teleport operations and with react this would be even easier. Not to bash on Phoenix, it’s best, I love it, but if you’re deeply nested in a component there should be a way to render stuff elsewhere to prevent collision or invalid HTML while live view keeps working correctly.
I recreated my situation in a very basic manner to outline the problem better:
defmodule MyAppWeb.EmailEditorLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
changeset = %{email_subject: "", email_body: ""}
{:ok,
socket
|> assign(:form, to_form(changeset))}
end
def render(assigns) do
~H"""
<div class="container mx-auto">
<h1>Email Editor</h1>
<.form for={@form} phx-submit="save">
<div class="space-y-4">
<div>
<.input field={@form[:email_subject]} label="Subject" />
</div>
<.live_component
module={MyAppWeb.RichEditorComponent}
id="rich-editor"
field={@form[:email_body]}
/>
<div>
<.button type="submit">Save Email</.button>
</div>
</div>
</.form>
</div>
"""
end
def handle_event("save", %{"email" => params}, socket) do
# Handle the main form submission
{:noreply, socket}
end
end
defmodule MyAppWeb.RichEditorComponent do
use MyAppWeb, :live_component
def mount(socket) do
{:ok,
socket
|> assign(:show_link_modal, false)
|> assign(:link_form, to_form(%{"url" => "", "text" => ""}))}
end
def render(assigns) do
~H"""
<div>
<div class="border p-2">
<div class="flex gap-2">
<button type="button" class="p-1 border rounded">Bold</button>
<button type="button" class="p-1 border rounded">Italic</button>
<button
type="button"
class="p-1 border rounded"
phx-click="show_link_modal"
phx-target={@myself}
>
Add Link
</button>
</div>
<div class="mt-2">
<textarea
name={@field.name}
class="w-full h-40 border rounded"
value={@field.value}
></textarea>
</div>
</div>
<.modal :if={@show_link_modal} id="link-modal" show>
<.form for={@link_form} phx-submit="save_link" phx-target={@myself}>
<div class="space-y-4">
<div>
<.input field={@link_form[:text]} label="Link Text" />
</div>
<div>
<.input field={@link_form[:url]} label="URL" />
</div>
<div class="flex justify-end gap-2">
<.button
type="button"
phx-click="cancel_link"
phx-target={@myself}
>
Cancel
</.button>
<.button type="submit">
Insert Link
</.button>
</div>
</div>
</.form>
</.modal>
</div>
"""
end
def handle_event("show_link_modal", _, socket) do
{:noreply, assign(socket, :show_link_modal, true)}
end
def handle_event("cancel_link", _, socket) do
{:noreply, assign(socket, :show_link_modal, false)}
end
def handle_event("save_link", %{"link" => params}, socket) do
# Handle the link form submission
{:noreply,
socket
|> assign(:show_link_modal, false)}
end
end