Tutorial

Tailwind Navbar New LiveView 0.18 components

With the latest Phoenix LiveView 0.18 release it has finally become time to revisit components. In the previous version, the components was a little hard to work with. The new things that has come, is that you can define attributes. Not only that, you can define the type of attribute, if it’s required and a default value.

Just by having a default value, makes it possible to display the components nicely in the upcoming LiveStoryBook. Besides that, Im planning to make components on all of the html elements in the SAAS Starter Kit so its easy to switch between CSS frameworks.

In this tutorial I want to create a Tailwind Navbar as a Phoenix Component. Usually, I have used DaisyUI components but for this tutorial, I have gone with the polished https://flowbite.com/ components.

Defining the components

Im going for a markup that looks something like:

<.navbar>
  <:logo>
    <.link navigate={"/"} class="flex items-center">
      <.logo />
    </.link>
  </:logo>
  <:link label="Home" to={"/home"} />
  <:link label="About" to={"/home"} />
</.navbar>

And the NavBar Im going to implement looks something like this:

Raw HTML

This example is based on the code from FlowBite but it could as well be Bootstrap. The Structure is the same.

<nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 rounded dark:bg-gray-900">
  <div class="container flex flex-wrap justify-between items-center mx-auto">
    <!-- LOGO -->
    <a href="/" class="flex items-center">
      <svg class="w-10 h-10 p-2 mr-3 text-white rounded-full bg-primary" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
        <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
      </svg>
      <span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">My App</span>
    </a>
    <!-- END LOGO -->

    <!-- MOBILE VISIBLE MENU BUTTON -->
    <button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 ml-3 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
      <span class="sr-only">Open main menu</span>
      <svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
    </button>
    <!-- END MOBILE VISIBLE MENU BUTTON -->

    <div class="hidden w-full md:block md:w-auto" id="navbar-default">
      <ul class="flex flex-col p-4 mt-4 bg-gray-50 rounded-lg border border-gray-100 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
        <li>
          <a href="#" class="block py-2 pr-4 pl-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 dark:text-white" aria-current="page">Home</a>
        </li>
        <li>
          <a href="#" class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">About</a>
        </li>
        <li>
          <a href="#" class="block py-2 pr-4 pl-3 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Contact</a>
        </li>
      </ul>
    </div>
  </div>
</nav>

Implementing the Logo component

I want to make a reusable logo component because I might use it in another places on the site. Normally that would be the Footer but could be in other places.

attr :name, :string, default: "My App"
def logo(assigns) do
  ~H"""
  <svg class="w-10 h-10 p-2 mr-3 text-white rounded-full bg-primary" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24">
    <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
  </svg>

  <span class="self-center text-xl font-semibold whitespace-nowrap dark:text-white">
    <%= @name %>
  </span>
  """
end

And then I can invoke it like:

<.logo />
OR
<.logo name="Example" />

Note that I don’t pass in the app name but could do it if I would to reuse the logo component between projects.

Navbar Component

slot :logo
slot :link, default: [%{__slot__: :link, inner_block: nil, label: "Home", to: "/home"}]
def navbar(assigns) do
  ~H"""
  <nav class="bg-white border-gray-200 px-2 sm:px-4 py-2.5 rounded dark:bg-gray-900">
    <div class="container flex flex-wrap items-center justify-between mx-auto">
      <%= render_slot(@logo) %>
      <button phx-click={toggle_dropdown("#navbar-default")} type="button" class="inline-flex items-center p-2 ml-3 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
        <span class="sr-only">Open main menu</span>
        <svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
      </button>
      <div class="hidden w-full md:block md:w-auto" id="navbar-default">
        <ul class="flex flex-col p-4 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
          <%= for link <- @link do %>
            <.link navigate={link.to} class="block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
              <%= link.label %>
            </.link>
          <% end %>
        </ul>
      </div>
    </div>
  </nav>
  """
end

Since I only allow for one logo-slot, I use <%= render_slot(@logo) %> to output it. However, regarding the links, these are in a list of slots that I want to loop though and display.

<%= for link <- @link do %>
  <.link navigate={link.to} class="block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
    <%= link.label %>
  </.link>
<% end %>

Toggle Dropdown JS

In mobile view, I need to be able to toggle the dropdown menu and make the menu links visible. In theory, this could be done with pure LiveView but there is now also a Javascript library that comes along. It has support for toggling the visibility of a component.

defp toggle_dropdown(id, js \\ %JS{}) do
  js
  |> JS.toggle(to: id)
end

And I can invoke the click event by using a phx-click and then pass in the function and the CSS selector to the element I want to toggle.

phx-click={toggle_dropdown("#navbar-default")}

Final Result

So, now in mobile view, I can see the burger menu in top right and when I click it, it expands and the menu items become visible just as you would expect.