Creating an Accordion in Phoenix Liveview

Hello friends, I wanted to spend some time and write about converting an HTML/JS accordion to Phoenix’s functional components in LiveView. We will go through what functional components are, slots, attrs and how to open/close without really writing any Javascript.

Background

I wanted to start with an HTML/JS based accordion so we can go through just how powerful LiveView is

A basic HTML/Javascript accordion can look something like this:

  <div class="max-w-md mx-auto">
    <div class="border rounded overflow-hidden">
      <!-- Accordion Item 1 -->
      <div class="border-b">
        <button
          class="w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none"
          onclick="toggleAccordion('content1')"
        >
          Section 1
        </button>
        <div id="content1" class="hidden p-4">
          <!-- Your content for Section 1 goes here -->
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        </div>
      </div>

      <!-- Accordion Item 2 -->
      <div class="border-b">
        <button
          class="w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none"
          onclick="toggleAccordion('content2')"
        >
          Section 2
        </button>
        <div id="content2" class="hidden p-4">
          <!-- Your content for Section 2 goes here -->
          Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
        </div>
      </div>

      <!-- Add more accordion items as needed -->

    </div>
  </div>

With the ability to toggle open/close:


  <script>
    function toggleAccordion(id) {
      const content = document.getElementById(id);
      content.classList.toggle('hidden');
    }
  </script>

That is all fine and dandy, but what if we want to reuse the accordion someplace else? We can convert this into a functional component!

Functional Components

Functonal components allow us to reuse components across various facets of our application, including LiveView and non-LiveViews(some people call them dead views).

You can learn more about the CoreComponents in the Phoenix Docs We will be using core_components.ex for our example.

We will then expand on the accordion to take in slots and attributes. Attributes are, as their name implies, attributes to pass to our component. These attrs are Slots are markup that we want our component to render in some way.

Hopefully this all makes more sense later on.

Getting Started

We can generate a new Phoenix app: mix phx.new --no-ecto my_app. Once finished, there is a new core_components.ex that was generated. These core components are all functional components and LiveView provides a few already(buttons, forms, etc.)

Let’s start up the app with iex -S mix phx.server

We need a canvas to paint our accordion onto, so we should create a new liveview

Inside router.ex add a new live route:

live "/hello", MyAppLive

Now create the new LiveView file at ./my_app/lib/my_app_web/live/my_app_live.ex

And paste the following in my_app_live.ex:

defmodule MyAppWeb.MyAppLive do
  use Phoenix.LiveView
  import MyAppWeb.CoreComponents

  def render(assigns) do
    ~H"""
      <h1>Hello there</h1>
    """
  end
end

If we visit http://localhost:4000/hello we should see our Hello there.

Converting HTML/JS Accordion to Functional Component

Conversion is pretty straightforward in our case, as HEEX templates are already HTML.

First we need to define our accordion component inside lib/my_app/components/core_components.ex and paste our original accordion without the JavaScript and onclick. We will re-add the click functionality later.

  def accordion(assigns) do
    ~H"""
      <div class="max-w-md mx-auto">
        <div class="border rounded overflow-hidden">
          <!-- Accordion Item 1 -->
          <div class="border-b">
            <button
              class="w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none"
              onclick="toggleAccordion('content1')"
            >
              Section 1
            </button>
            <div id="content1" class="p-4">
              <!-- Your content for Section 1 goes here -->
              Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            </div>
          </div>

          <!-- Accordion Item 2 -->
          <div class="border-b">
            <button
              class="w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none"
              onclick="toggleAccordion('content2')"
            >
              Section 2
            </button>
            <div id="content2" class="p-4">
              <!-- Your content for Section 2 goes here -->
              Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
            </div>
          </div>

          <!-- Add more accordion items as needed -->

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

We can now use our accordion in our LiveView! All we need to do is replace our <h1>Hello there</h1> with <.accordion></.accordion>. We should now see our accordion.

Adding Slots and Attributes to our Functional Component

Having hard coded markup specific to this accordion is undesired if we want to reuse it in other places. We can use slots and attrs to render mark up being passed into the component.

Slots and attrs need to be defined above our accordion definition and will look like the following:

  slot :content do
    attr :title, :string
    attr :id, :string
  end

  def accordion(assigns) do
    ~H"""
    <div class="max-w-md mx-auto">
      <div class="border rounded overflow-hidden">
        <%= for content <- @content do %>
          <div class="border-b">
            <button
              class="w-full text-left py-2 px-4 font-semibold bg-gray-200 hover:bg-gray-300 focus:outline-none"
            >
              <%= content.title %>
            </button>
            <div id={content.id} class="p-4">
              <%= render_slot(content) %>
            </div>
          </div>
        <% end %>
      </div>
    </div>
    """
  end

Here, we are using a named slot called content that has a few attributes that will help us when we add the open/close functionality back.

We need to update our LiveView to use the new accordion functionality:

    <.accordion>
      <:content title="my title" id="my-title">
        <h1>Hello there</h1>
      </:content>
      <:content title="my second title" id="my-second-title">
        <h1>Hello there x2</h1>
      </:content>
      <:content title="my third title" id="my-third-title">
        <h1>Hello there x3</h1>
      </:content>
    </.accordion>

Currently, we cannot open/close it, as we are not using the JavaScript that came with it. So let’s do that now!

Replacing JavaScript with LiveView JS

Okay, so, this is technically cheating as we are still using JavaScript. LiveView has a module called JS. This module is full of utility functions that will call the JavaScript for us without us having to write a script for it.

We need to add a phx-click to the button in the accordion:

phx-click={JS.toggle(to: "##{content.id}")}

JS.toggle() will

Show or hide elements based on visibility, with optional transitions

Condiontally Show/Hide by default

If we wanted to start with the accordion panels closed by default we can use an attr called start_closed

Add the attr to the content slot:

  slot :content do
    attr :title, :string
    attr :id, :string
    attr :start_closed, :boolean
  end

And add a condtional style to the accordion panel:

<div id={content.id} class="p-4" style={if content[:start_closed], do: "display: none;"}>

Note how we have to access the start_closed, currently we cannot set default values for slot attributes

Finally, add the attribute to some of the accordion’s content slots:

<:content title="my second title" id="my-second-title" start_closed={true}>
<:content title="my third title" id="my-third-title" start_closed={true}>

We have parts of the accordion that are hidden by default while retaining the ability to toggle as needed!

Conclusion

Functional components are super powerful and allow us to reuse components across our applcation. Slots and attributes can be somewhat intimidating at first but hopefully this helped to bring you the confidence you need.

Back