My Favorite new LiveView Feature

A purple-ish pink-ish sunny sky with happy fluffy clouds.
Image by Annie Ruygt

We’re Fly.io. We run apps for our users on hardware we host around the world. Fly.io happens to be a great place to run Phoenix applications. Check out how to get started!

Phoenix LiveView v0.20.4 was released in February 2024. It included a new feature that hasn’t gotten enough recognition. This simple little feature is a nice improvement for developer experience. Where previously I used Alpine.js or needed to craft a custom hook, now it’s built-in!

What feature is it?

It is 🥁 drum-roll 🥁… JS.toggle_class/1

🥱 (yawn)

Don’t give me that! It’s actually really helpful. And it’s surprisingly versatile. Let’s take a closer look!

These examples all rely on Tailwind CSS classes that are being toggled.

Basic visibility

Let’s start with a basic example of toggling the visibility of an element.

Animated gif showing the text 'Visible content' that, when clicked, reveals more text saying 'Toggled content.'

The code for this is all in the DOM.

<div
  phx-click={JS.toggle_class("hidden", to: "#toggled-content")}
  class="px-4 py-3 cursor-pointer bg-purple-100 rounded-md border border-purple-300 text-purple-600"
>
  <div class="font-medium">Visible content</div>
  <div id="toggled-content" class="hidden mt-2 text-purple-800 font-bold">
    Toggled content
  </div>
</div>

The phx-click uses JS.toggle_class/1 to toggle the hidden class on the DOM element with the #toggled-content id. In the example, the element starts off hidden. Clicking the element either removes the hidden class or adds it back.

But what if we want to toggle multiple items with the same click?

Toggle multiple elements at once

In this example, a single click toggles classes on two different items. Here’s what it looks like:

Animated gif showing text 'Click to reveal text' that, when clicked, the text is replaced with a paragraph of lorem ipsum text.

When the user clicks the “Click to reveal text” text, it is hidden and a previously hidden element is revealed.

<div
  id="example-2"
  phx-click={
    JS.toggle_class("hidden",
      to: ["#example-2 > .title-content", "#example-2 > .expanded-content"]
    )
  }
  class="px-4 py-3 cursor-pointer bg-purple-100 rounded-md border border-purple-300 text-purple-600"
>
  <div class="title-content font-medium">Click to reveal the text</div>
  <div class="expanded-content hidden mt-2 text-purple-800 text-md font-light">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
  </div>
</div>

The JS.toggle_class/3 function takes a “to” option which supports a list of CSS selectors for selecting a DOM element to toggle the hidden class on.

Notice that we’ve added the .title-content and .expanded-content classes to the div elements solely for use as selectors. Clicking the element looks for an immediate child element with those classes and toggles the hidden class.

In the example, a single click adds the hidden class to one element and removes it from another at the same time.

Toggle with animations

We can toggle more than just the hidden class! Tailwind makes it easy to add animations based solely on classes. In this next example, we’ll animate a “down” chevron to rotate when clicked making it point “up”.

Animated gif showing text 'Section Title' that, when clicked, expand to show 3 lines of 'Cool extra content!'. A downward pointing chevron spins with animation to point up.

Here’s the code:

<div
  id="example-3"
  phx-click={
    JS.toggle_class("hidden", to: "#example-3 > .expanded-content")
    |> JS.toggle_class("rotate-180", to: "#example-3 .chevron", time: 300)
  }
  class="px-4 py-3 cursor-pointer bg-purple-100 rounded-md border border-purple-300 text-purple-600"
>
  <div class="flex justify-between">
    <div class="font-medium">
      Section Title
    </div>
    <div class="chevron transition-all duration-300">
      <.icon name="hero-chevron-down" class="text-lg" />
    </div>
  </div>
  <div class="expanded-content hidden mt-2 text-purple-800 text-md font-light">
    <p>Cool extra content!</p>
    <p>Cool extra content!</p>
    <p>Cool extra content!</p>
  </div>
</div>

The code toggles the Tailwind CSS class rotate-180 from an item. It uses the ‘time’ option to express a 300 ms duration for the change.

The rotating item is a div with the chevron class, which includes other Tailwind CSS animation classes like transition-all and duration-300.

Clicking the element toggles the visibility and animates the “down” chevron to rotate and now point upwards, indicating that clicking it again will collapse the content.

Subtle, but effective. Most importantly, it was very easy.

Dynamic items in a list

All the examples to this point used a hard-coded DOM id. Frequently we’ll have lists of items that we want to support where we need a dynamically assigned id.

Let’s see an example of that next.

Animated gif showing a list of 3 dynamic items. The titles are 'Famous Quote', 'UK Saying', and 'I know...'. When clicked it reveals additional text like 'Keep calm and carry on. ~Winston Churchill'

Typically, data like this would be loaded from a database. Here’s our list of items:

items = [
  %{
    id: 1,
    title: "Famous Quote",
    details: "If you judge people, you have no time to love them. ~Mother Teresa"
  },
  %{id: 2, title: "UK Saying", details: "Keep calm and carry on. ~Winston Churchill"},
  %{id: 3, title: "I know...", details: "...more than I did yesterday."}
]

Here’s how we use the list of items in our template:

<h2 class="mb-2 text-lg font-medium text-purple-900">
  Dynamic Items
</h2>
<ul class="grid grid-cols-1 gap-4">
  <li
    :for={item <- items}
    id={"item-#{item.id}"}
    class="px-4 py-3 cursor-pointer bg-purple-100 rounded-md border border-purple-300 text-purple-600"
    phx-click={JS.toggle_class("hidden", to: "#item-#{item.id} > .expanded-content")}
  >
    <div class="font-medium">
      <%= item.title %>
    </div>
    <div class="expanded-content hidden mt-2 text-purple-800 text-md font-light">
      <%= item.details %>
    </div>
  </li>
</ul>

The expression :for={item <- items} dynamically generates an <li> element for each item in the list. We use the item map to dynamically build a DOM id for the element: id={"item-#{item.id}"}.

The phx-click event uses the same approach to assemble the DOM id to find the element and toggle the hidden class.

Reusing the click event

When multiple elements in a component should all trigger the same event, duplicating a complex phx-click JS event can quickly become tedious. Fortunately, there’s a simple way to define the click event and share it among multiple elements.

In the view code, we can define a private function like this:

defp expand_item(item_id) do
  JS.toggle_class("hidden", to: "#item-#{item_id} > .expanded-content")
  |> JS.toggle_class("rotate-180", to: "#item-#{item_id} .chevron", time: 300)
end

In our template, multiple elements can share the same phx-click event:

phx-click={expand_item(item)}

Defining the actions once and reusing them this way makes it easier to maintain when more complex interactions are needed.

Discussion

A major benefit to using JS.toggle_class/1 is avoiding the need to pull in another tool just because “it makes things easier.”

For simple visibility changes and animations like those demonstrated above, JS.toggle_class/1 is perfect! It’s simple, extensible, and best of all, it’s built-in!

Will this become a new favorite LiveView feature of yours?

Fly.io ❤️ Elixir

Fly.io is a great place to run your Phoenix LiveView apps. It’s easy to get started. You can be running in minutes.

Deploy a Phoenix app today!