#How can I move the modal component to the body ?

14 messages · Page 1 of 1 (latest)

lethal plover
#

do you really have to move the modal to the body? why is that necessary?

lusty sedge
#

I have a layout that is seperated as topbar sidebar and main layout, the modal is in the main layout so it renders in a position that I dont want

lethal plover
#

but the default CSS for the default modal component renders it absolute and centered in the browser, it doesn't matter where it is actually rendered in the HTML

lusty sedge
#

Yeap, I also tried fixed position but that didn't seem to work..

Sounds like needs a refactor ?

jade hatch
#

have you tried the dialog element?

lethal plover
#

the hook you showed above is just going to break a lot of things and I wouldn't trust doing things like that. In a normal phoenix app using default components, it doesn't matter where in the DOM the modal is rendered, it will always appear absolutely positioned and in the center of the browser

#

if this isn't how your modal looks, then you must have broken the styling

lusty sedge
#

here is the smallest repro

<div class="flex h-40 lg:h-32 ">
  <!-- Topbar -->
 <div class="flex ">
  </div>
</div>
<!-- Main Layout -->
<div id="app" class="flex flex-grow">
  <CanaryWeb.Navigation.navigation current_user={@current_user} current_tenant={@current_tenant} />
  <!-- Main Content -->
  <main class="bg-stone-50 flex flex-col -translate-y-12 lg:-translate-y-16 px-6 py-4 rounded-xl w-full max-w-full">
    <%= @inner_content %>
  </main>
</div>
lusty sedge
lusty sedge
jade hatch
#

This is what I use:

  attr :id, :string, required: true
  attr :title, :string
  attr :class, :string, default: ""
  attr :cancel_text, :string, default: "Cancel"
  attr :cancel_class, :string, default: "outline"

  slot(:inner_block, required: true)
  slot(:action_row)

  def modal_dialog(assigns) do
    ~H"""
    <dialog id={@id} class={["modal", @class]} data-keep="open" data-keep="open">
      <div
        class="modal-content modal--md pane"
        phx-click-away={close_modal_dialog(@id)}
        phx-window-keydown={close_modal_dialog(@id)}
        phx-key="escape"
      >
        <div class="modal-header">
          <h2><%= @title %></h2>
          <a href="#" class="phx-modal-close" phx-click={close_modal_dialog(@id)}>&nbsp;</a>
        </div>

        <%= render_slot(@inner_block) %>

        <%= if assigns[:action_row] do %>
          <div class="confirmation-buttons">
            <button type="reset" class={["button push-right ", @cancel_class]} phx-click={close_modal_dialog(@id)}>
              <%= @cancel_text %>
            </button>
            <%= render_slot(@action_row) %>
          </div>
        <% end %>
      </div>
    </dialog>
    """
  end

  def close_modal_dialog(id) do
    JS.dispatch("felt:close_modal", detail: %{id: id})
  end

  def close_modal_dialog(%Phoenix.LiveView.Socket{} = socket, id) do
    Phoenix.LiveView.push_event(socket, "app:close_modal", %{id: id})
  end

  def show_modal_dialog(id) do
    JS.dispatch("felt:show_modal", detail: %{id: id})
  end

  def show_modal_dialog(%Phoenix.LiveView.Socket{} = socket, id) do
    Phoenix.LiveView.push_event(socket, "app:show_modal", %{id: id})
  end
const showModal = (event: CustomEvent) => {
  const modalElement = document.getElementById(
    event.detail.id as string
  ) as HTMLDialogElement | null;

  if (modalElement) {
    modalElement.showModal();
  }
};

const closeModal = (event: CustomEvent) => {
  const modalElement = document.getElementById(
    event.detail.id as string
  ) as HTMLDialogElement | null;

  if (modalElement) {
    modalElement.close();
  }
};

window.addEventListener("app:show_modal", showModal as EventListener);
window.addEventListener("phx:app:show_modal", showModal as EventListener);

window.addEventListener("app:close_modal", closeModal as EventListener);
window.addEventListener("phx:app:close_modal", closeModal as EventListener);

And a config for new LiveSocket to avoid re-renders from closing it:

dom: {
      onBeforeElUpdated(from, to) {
        if (from.matches("[data-keep]")) {
          for (const attr of from.attributes) {
            if (from.dataset.keep?.split(" ").includes(attr.name)) {
              to.setAttribute(attr.name, attr.value);
            }
          }
        }

        return true;
      },
    },
#

works great with the one exception that you need to be a bit careful if you put it in eg a dropdown where it will be hidden when the dropdown loses focus

#

Usage example:

  <.form :let={f} id="update-name-form" for={@update_name_form} phx-submit="update_name">
    <.modal_dialog id="update-name-modal" title="Change name">
      <div phx-feedback-for={input_name(f, :name)}>
        <%= text_input(f, :name,
          phx_hook: "autoFocus",
          placeholder: "name",
          phx_debounce: "blur"
        ) %>
        <%= label(f, :name, "Name") %>
        <%= error_tag(f, :email) %>
      </div>

      <:action_row>
        <%= submit("Save", class: "button pink", phx_disable_with: "Saving...") %>
      </:action_row>
    </.modal_dialog>
  </.form>
lusty sedge
#

thanks, looks promising but I will definitely use a custom dropdown in it but I can give it a try