Closing a Modal only after server side operation was successful

For a modal with a form to change the user name, there are basically two scenarios when submitting the form:

  1. The validation was successful → The server persists the name change → The modal is no longer needed
  2. The validation fails → Errors are displayed on the form → The modal is still required so the user can fix the error

This means the backend result decides wether the modal can be closed or not.

I could add an event listener on the window in app.js, dispatch an event in the backend on successful validation/persistance and close a specific or all modals on receiving the event.

But I was wondering if there is a way to tell the frontend to close the modal using hide_modal/2 function from the core components instead of adding a second variant to app.js.

LiveView version is 0.19.5

The easiest way is to annotate the socket with push_navigate and reload the same view they’re already on.

case perform_action(params) do
  {:ok, _} ->
    socket
    |> put_flash(:info, "Saved new info successfully!")
    |> push_navigate(to: ~p"/the/route")

  {:error, changeset} ->
    assign(socket, :form, to_form(changeset)}
end

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2

3 Likes

Here’s an alternative that takes advantage of LiveView’s state management and element bindings:

<button phx-click={JS.push("edit")}>Edit</button>
<.modal 
  id="user-form"
  phx-mounted={show_modal()}
  phx-remove={hide_modal()}
  :if={@form}></.modal>

In your LiveView:

def handle_event("edit", _params, socket) do
  {:noreply, assign(socket, :form, build_form())}
end

def handle_event("update", %{"user" => user_params}, socket) do
  # after update
  {:noreply, assign(socket, :form, nil)}
end

Setting/unsetting the form will show and hide the modal, and you can use phx-mounted and phx-remove to handle CSS transitions (LiveView has some magic to wait until the transitions have completed before removing the element from the DOM)

2 Likes

I went with a different approach as outlined in this fly.io article.

The idea is basically to

  1. embed some JS as an attribute on the DOM element
  2. push an event from the backend to trigger that JS, passing the target DOM and attribute
  3. Have an event listener on the frontend (app.js) that searches the DOM element, finds the attribute and executes the embedded JS.