Skip to content

Instantly share code, notes, and snippets.

@mxgrn
Last active June 15, 2024 14:18
Show Gist options
  • Save mxgrn/cb114e52d2a586aeb36241727ec77ac9 to your computer and use it in GitHub Desktop.
Save mxgrn/cb114e52d2a586aeb36241727ec77ac9 to your computer and use it in GitHub Desktop.
LiveView modal close confirmation on dirty form

Problem: accidentally pressing 'Esc' when editing a form in a "standard" LiveView modal dialog will discard all edits.

Proposed solution: Make the modal show a confirmation window on close when there have been changes on the form.

LiveView v0.20 was used.

Table of content

Making LiveView modal show confirmation on close

Make modal accept close_confirm attr

This is how we'll be passing the confirmation message to the modal when a confirmation is needed.

In core_components.ex, add:

  attr :close_confirm, :string, default: nil

Make data-cancel attribute call custom JS

Let's pass close_confirm down to custom JS with JS.dispatch at the attempt to close the modal.

In core_components.ex, replace this line:

  data-cancel={JS.exec(@on_cancel, "phx-remove")}

with these:

  data-do-cancel={JS.exec(@on_cancel, "phx-remove")}
  data-cancel={JS.dispatch("modal:maybe_close", detail: %{close_confirm: @close_confirm})}

In app.js add the handler for our dispatched event:

window.addEventListener("modal:maybe_close", (e) => {
  let el = e.detail.dispatcher

  let doClose = () => {
    // Call the former default action thet we previously renamed to "data-do-cancel"
    liveSocket.execJS(el, el.getAttribute("data-do-cancel"))
  }

  if (e.detail.close_confirm) {
    if (confirm(e.detail.close_confirm)) {
      doClose()
    }
  } else {
    doClose()
  }
})

Using updated modal in a sample CRUD LiveView

Let's say we used mix phx.gen.live to generate some CRUD code for managing posts.

Mark forms as dirty

Add a new boolean assign into MyApp.PostLive.Index to mark the form as "dirty":

In live/post_live/index.ex add a new handle_info clause to react on the form being changed:

def handle_info(:changed, socket) do
    {:noreply, assign(socket, :dirty_form, true)}
end

We need to make sure this gets called from live/post_live/form_component.ex. Right below handle_event for the "validate" event, add this line:

send(self(), :changed)

We also need to reset the dirty_form flag each time we re-open the modal. Back in live/post_live/index.ex, reset the assign in apply_action for both :edit and :new actions, e.g., for :edit:

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    # ...
    |> assign(:dirty_form, false)
  end

Use the dirty_form flag to configure the modal

In live/post_live/index.html.heex, when defining the modal, add our new config option that we added in the beginning:

<.modal
  :if={@live_action in [:new, :edit]}
  id="post-modal"
  show
  on_cancel={JS.patch(~p"/posts")}
  close_confirm={@dirty_form && "Are you sure?"}
>

That's it. For any questions, contact me on X, or better yet, hire me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment