Toggle a modal from a Phoenix LiveView

In the last couple of years I’ve quite often found myself wanting to open a modal from the backend. I totally forgot how I did it last time though, so here is it written down so I’ll remember for all time.

man vs potato as imagined by the AI

The first thing we’ll to do is add an event listener to our app.js so we can push events to the client from the liveview to open and close the modal.

This will take the id of a DOM element, the name of an attr on that element, and then execute the js code we find there. We’ll call it js-exec and prefix it with the required prefix

// app.js
window.addEventListener("phx:js-exec", ({ detail }) => {
    document.querySelectorAll(detail.id).forEach(el => {
        liveSocket.execJS(el, el.getAttribute(detail.attr))
    })
})

Then we add some data attributes to the modal component. This was generated for us by Phoenix and is located in in core_components.ex

Lets call them data-show-modal and data-close-modal and use the helper functions already provided to actually show and hide the modal.

  // core_components.ex
  def modal(assigns) do
    ~H"""
    <div
     id={@id}
      phx-mounted={@show && show_modal(@id)}
      phx-remove={hide_modal(@id)}
+      data-show-modal={show_modal(@id)}
+      data-close-modal={hide_modal(@id)}
      data-cancel={JS.exec(@on_cancel, "phx-remove")}
      class="relative z-50 hidden"
    >...

Also, in our liveview’s render function, we add a modal that does some cool stuff.

def render(assigns) do
  ~H"""
  <.modal id="my-cool-modal">
    ... cool stuff goes here
  </.modal>
  """
end

Then we push an event from somewhere to execute data-close-modal on our modal.

def handle_event("show_cool_modal", _, socket) do
  {:noreply,
    socket
    |> assign(cool_stuff_to_show: ...)
    |> push_event("js-exec", %{
      id: "#my-cool-modal",
      attr: "data-show-modal"
    })}
end

And there we go! We can now open and close modals based on logic in our elixir code. You could for instance throw the push_event into a handle_info function instead, and annoy users based on pubsub messages.

The possibilities truly are endless!