Use LiveView.JS to toggle menu hamburger to show/hide menu options

I’m trying to use LiveViewJS to show/hide hamburger menu options (inspired to do the same as in this video from Tailwind CSS founder where he was using VueJS):

  • on mobile screen I’ d like to display a hamburger menu
  • when clicking on a hamburger menu icon a list of menu options/links should be displayed and the icon should change from ‘hamburger’ to the ‘close’
  • on larger screens, I’d like to display menu options in line.

Kind of a classic layout, right?

I know that I need to check a boolean value in some way to show either the ‘hamburger’ or ‘close’ icon and took a try like that in the root.html.heex template:

<header class="bg-gray-900">
      <div class="flex items-center justify-between px-4 py-3">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke-width="1.5"
            stroke="currentColor"
            class="h-8 text-white"
          >
            <path :if={@show_menu}
              stroke-linecap="round"
              stroke-linejoin="round"
              d="M6 18L18 6M6 6l12 12"
            />
            <path :if={!@show_menu}
              stroke-linecap="round"
              stroke-linejoin="round"
              d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"
            />
          </svg>
        </div>
        <div>
          <button
            type="button"
            class="text-gray-500 hover:text-white focus:text-white focus:outline-none"
            phx-click={toggle_navbar_menu("#menu")}
          >
            <svg
              class="w-6 h-6 fill-current"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="1.5"
              stroke="currentColor"
              class="w-6 h-6"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
              />
            </svg>
          </button>
        </div>
      </div>
      <div id="menu" class="px-2 pt-2 pb-4">
        <a
          href="#"
          class="block px-2 py-1 font-semibold text-white rounded hover:bg-gray-800"
          >List you property
        </a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800"
          >Trips
        </a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800"
          >Messages
        </a>
      </div>
    </header>
...
# the rest ot the template

Then in core_components.ex added this function:

def toggle_navbar_menu(js \\ %JS{}, selector) do
    js
    |> JS.toggle(to: selector) #=> will not do the trick!
  end

The above function will not do the trick because:

  • JS.toggle/1 function will just toggle the element visibility by toggling its display: block attribute
  • the variable @show_menu is never set/changed

The question I have is how to achieve this behavior.
Which function in JS module to use?
How to set a boolean variable to be able to reuse in the template?

Go for show and hide. You don’t need or want server side state for this sort of thing. The opened/closed state is the visibility of the open/close controls.

Imagine the initial state is closed, there is an icon to click which hides itself, shows the menu, an overlay and a close button. Clicking the close button or overlay shows the open button and hides the menu, overlay and close button.

Sure, I don’t want to manage this state on the server side, that’s why I’d like to use LiveView.JS functions.
So here is what I managed to do at this moment:

  • I declared a custom fucntion in layouts.ex (no more in core_components because the related HTML code is located in the root.html.heex lauout template):
  def toggle_navbar_menu(js \\ %JS{}) do
    js
    |> JS.toggle(to: "#menu", in: "fade-in-scale", out: "fade-out-scale")
  end
  • Then called this function in the root.html.heex layout:
<button
            type="button"
            class="text-gray-500 hover:text-white focus:text-white focus:outline-none"
            phx-click={toggle_navbar_menu()}
          >
....
  • then modified the class of the div containing the menu links to be hidden by default:
<div id="menu" class="hidden px-2 pt-2 pb-4 sm:flex">
        <a
          href="#"
          class="block px-2 py-1 font-semibold text-white rounded hover:bg-gray-800"
          >List you property</a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800 sm:mt-0 sm:ml-2"
          >Trips</a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800 sm:mt-0 sm:ml-2"
          >Messages</a>
      </div>

It works on a mobile screen, - the button does open/hide the menu links, but once I resized the screen to a larger one, the links are no more displayed until I refresh the page. I only happens in case of I trigger the hamburger button. If I just resize the screen larger-mobile/mobile-larger without triggering the button, the menu links are correctly displayed. I can see that the style of the div containing the links was set tp display:none in case of actioning the hamburger button and stayed the same after the resizing to a larger screen.
Here is the full content of the root.html.heex layout:

<!DOCTYPE html>
<html lang="en" style="scrollbar-gutter: stable;">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title suffix=" · Phoenix Framework">
      <%= assigns[:page_title] || "LiveDraft" %>
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class="antialiased bg-white">

    <header
      class="bg-gray-900 sm:flex sm:justify-between sm:px-4 sm:py-3 sm:items-center"
    >
      <div class="flex items-center justify-between px-4 py-3 sm:p-0">
        <div>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke-width="1.5"
            stroke="currentColor"
            class="h-8 text-white"
          >
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"
            />
          </svg>
        </div>
        <div class="sm:hidden">
          <button
            type="button"
            class="text-gray-500 hover:text-white focus:text-white focus:outline-none"
            phx-click={toggle_navbar_menu()}
          >
            <svg
              class="w-6 h-6 fill-current"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke-width="1.5"
              stroke="currentColor"
              class="w-6 h-6"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
              />
            </svg>
          </button>
        </div>
      </div>
      <div id="menu" class="hidden px-2 pt-2 pb-4 sm:flex">
        <a
          href="#"
          class="block px-2 py-1 font-semibold text-white rounded hover:bg-gray-800"
          >List you property</a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800 sm:mt-0 sm:ml-2"
          >Trips</a>
        <a
          href="#"
          class="block px-2 py-1 mt-1 font-semibold text-white rounded hover:bg-gray-800 sm:mt-0 sm:ml-2"
          >Messages</a>
      </div>
    </header>

    <%= @inner_content %>
  </body>
</html>