Elixir/Phoenix: fenêtre modale avec LiveComponent et hook JavaScript

Publié le 2021-04-25

Notre page principale se compose d'une LiveView window_live.ex, d'un LiveComponent modal_component.ex statefull. L'idée est d'utiliser un LiveComponent qui affiche du contenu avec un évènement provenant de window_live.ex et se ferme avec un évènement provenant du client (via un hook JavaScript très simple sans eventHandler).

Ici push_event/3 n'est pas utilisé dans le LiveView, ni handleEvent et addEventListener dans le hook JavaScript. En effet nous utilisons updated() qui est exécuté lors d'un rafraîchissement d'un élément du DOM (ici notre LiveComponent qui contient la fenêtre modale). De plus il n'y a pas besoin d'utiliser la propriété CSS display.

Le LiveView window_live.ex:

defmodule MyAppWeb.WindowLive do
    use MyAppWeb, :live_view


    def render(assigns) do
        ~L"""
            <%= live_component @socket, MyAppWeb.ModalComponent, id: :modal %>
            <%= link "Ouvrir", to: "#", "phx-click": "openmodal" %>
        """
    end

    def mount(_params, session, socket) do
        {:ok, socket |> assign(:modal, nil)}
    end

    # Évènement `closemodal` du hook (client) pour fermer la fenêtre modale (LiveComponent)
    def handle_event("closemodal", _params, socket) do

        #Modifie socket.assigns.modal et force MyAppWeb.ModalComponent à se rafraîchir
        send_update(MyAppWeb.ModalComponent, id: :modal, modal: nil)
        {:noreply, socket}
    end

    # Évènement `openmodal` rafraîchit le LiveComponent (avec un changement de valeur de modal)
    def handle_event("openmodal", _params, socket) do
        send_update(MyAppWeb.ModalComponent, id: :modal, modal: "test")
        {:noreply, socket}
    end

end

Le LiveComponent modal_component.ex qui affiche obligatoirement une div dont l'id est modal avec phx-hook="ModalHook". Il n'y a pas de contenu lorsque socket.assigns.modal == nil.

defmodule MyAppWeb.ModalComponent do
    use MyAppWeb, :live_component

    def render(assigns) do
      ~L"""
      <div id="modal" phx-hook="ModalHook">

        <%= if not is_nil(@modal) do %>

            <div id="modal-window" class="modal">
                <div class="modal-content">
                    <span class="close">&times;</span>
                    
                    Le contenu de la fenêtre modale.
                </div>
            </div>

        <% end %>
        </div>
      """
    end

    def mount(socket) do
        {:ok, socket |> assign(:modal, nil)}
    end
end

Note: il est possible d'envoyer l'évènement openmodal vers MyAppWeb.ModalComponent au lieu de MyAppWeb.WindowLive en ajoutant "phx-target": "#modal" dans lien qui génère l'évènement. Ne pas oulier handle_event/3 dans MyAppWeb.ModalComponent pour réceptionner l'évènement.

Le hook JavaScript dans assets/js/app.js:

let Hooks = {};
Hooks.ModalHook = {
    // Exécuté lorsque le LiveComponent a été rafraîchi
    // pas besoin de handleEvent/addEventListener ni signaux.
    updated(){
        let self = this;
        var modal = document.getElementById("modal-window");

        // S'assure que la fenêtre modale possède un contenu (lorsque socket.assigns.modal != nil)
        if(modal !== null) {
            var span = document.getElementsByClassName("close")[0];

            // Un clique sur la croix: envoyer le signal closemodal à window_live.ex
            span.onclick = function() {
                self.pushEvent("closemodal", {});
            }

            // Un clique à l'extérieur de la fenêtre modale: 
            // envoyer le signal closemodal à window_live.ex
            window.onclick = function(event) {
                if (event.target == modal) {
                    self.pushEvent("closemodal", {});
                }
            }
        }
    }
}

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})

La partie CSS à jouter dans priv/static/css/myapp.css (pensez à inclure le fichier dans le layout lib/my_app_web/templates/layout/root.html.leex).

.modal {
  position: fixed;
  z-index: 1;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%; 
  overflow: auto;
  background-color: rgb(0,0,0);
  background-color: rgba(0,0,0,0.4);
}

.modal-content {
  background-color: #fefefe;
  margin: 15% auto;
  padding: 20px;
  border: 1px solid #888;
  width: 80%;
}

.close {
  color: #aaa;
  float: right;
  font-size: 28px;
  font-weight: bold;
  line-height:25px;
}

.close:hover,
.close:focus {
  color: black;
  text-decoration: none;
  cursor: pointer;
} 

Liens complémentaires