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.
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:
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”.
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.
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?