How to show a modal from the server and hide with Phoenix.LiveView.JS and repeat that

I want to trigger the show of a modal from the server every 2 seconds. The user can hide it by clicking on it but after another 2 seconds it should reappear.

With this code I can show the modal once and have the user hide it but not again. Not in a loop.

Is that possible at all? How?

defmodule Demo3Web.TestLive do
  use Demo3Web, :live_view
  @refresh_rate 2000

  alias Phoenix.LiveView.JS

  def mount(_params, _session, socket) do
    if connected?(socket), do: Process.send_after(self(), :tick, @refresh_rate)

    socket =
      socket
      |> assign(show_modal: false)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <%= if @show_modal == true do %>
      <div id="modal" class="phx-modal" phx-remove={hide_modal()}>
        <div
          id="modal-content"
          class="phx-modal-content"
          phx-click-away={hide_modal()}
          phx-window-keydown={hide_modal()}
          phx-key="escape"
        >
          <button class="phx-modal-close" phx-click={hide_modal()}>✖</button>
          <p>Time is up. Again!</p>
        </div>
      </div>
    <% end %>
    """
  end

  def hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(to: "#item")
    |> JS.hide(transition: "fade-out", to: "#modal")
    |> JS.hide(transition: "fade-out-scale", to: "#modal-content")
  end

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, @refresh_rate)

    socket =
      socket
      |> assign(show_modal: true)

    {:noreply, socket}
  end
end

@show_modal is never set to false after you set it to true. So you hide it on the client and then it stays hidden.

You need to make it so closing the modal still sends an event to the server, so you actually hide it on the server too. There is JS.push.

Alternatively, move the whole logic to JS. Right now the control is split both ways. The best way to use the JS stuff is to:

  1. Do actions earlier in the browser that would be done by the server anyway (similar to optimistic UI)

  2. Do actions fully on the client without involving the server

1 Like

Thanks @josevalim !

I updated the example code accordingly.

defmodule Demo3Web.TestLive do
  use Demo3Web, :live_view
  @refresh_rate 5000

  alias Phoenix.LiveView.JS

  def mount(_params, _session, socket) do
    if connected?(socket), do: Process.send_after(self(), :tick, @refresh_rate)

    socket =
      socket
      |> assign(show_modal: false)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <%= if @show_modal == true do %>
      <div id="modal" class="phx-modal" phx-remove={hide_modal()}>
        <div
          id="modal-content"
          class="phx-modal-content"
          phx-click-away={hide_modal()}
          phx-window-keydown={hide_modal()}
          phx-key="escape"
        >
          <button class="phx-modal-close" phx-click={hide_modal()}>✖</button>
          <p>Time is up. Again!</p>
        </div>
      </div>
    <% end %>
    """
  end

  def hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(to: "#item")
    |> JS.hide(transition: "fade-out", to: "#modal")
    |> JS.hide(transition: "fade-out-scale", to: "#modal-content")
    |> JS.push("clicked")
  end

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, @refresh_rate)

    socket =
      socket
      |> assign(show_modal: true)

    {:noreply, socket}
  end

  def handle_event("clicked", _, socket) do
    socket =
      socket
      |> assign(show_modal: false)

    {:noreply, socket}
  end
end

Another way of showing the modal from the server is to push_event/3 to a hook and then liveSocket.execJS.

https://hexdocs.pm/phoenix_live_view/0.17.2/js-interop.html#executing-js-commands