Wanting to show/hide content on a page is an incredibly common task, and we're probably all familiar with the traditional ways of doing this using JavaScript. While there's nothing wrong with this, it may not be the best approach in all cases. For example, one major drawback is that if you reload the page, it will be back in its default state. Something you've hidden will be shown again, or vice versa.
So what if we want something we hide to stay hidden the next time we come back to a page? 🤔
I'm glad you asked! That's exactly what I'm going to show you how to do in Rails, using Turbo Frames and the newly released gem Kredis. We'll be toggling content in a way that persists across page refreshes/navigation, is super quick to implement, and best of all, requires 0 lines of JavaScript 🙌.
Prerequesites: You'll need a Rails application, version 6 or higher, with Hotwire/Turbo installed and a Redis connection. It should also have authentication in place (Devise, etc.) and a User model.
The main elements of this solution are as follows:
- Use Kredis to add a Redis-backed attribute on our User model to store the state
- Wrap the content in a
turbo_frame_tag
, including a link that will function as a hide/show button - Update the Kredis attribute in our controller and re-render the turbo frame
That's it!
So for our example, let's say we have a chart on our Dashboard. Some users may want to see it every time they log in, others might prefer to have it hidden.
Step 1: add a Kredis attribute to our User model
Once you've gone through the simple steps to install the Kredis gem, we're ready to add the Kredis attribute to our User model.
class User < ApplicationRecord
...
kredis_boolean :hide_dashboard_chart
...
def hide_dashboard_chart?
!!hide_dashboard_chart.value
end
end
I've opted for hide
here because we want the default behavior to be showing the chart, and hiding it to be an opt-in action. I've also added a helper method that makes accessing the value slightly easier and also accounts for the initial value being nil
(which we'll treat as false).
Step 2: Wrap the content we want to show/hide in a turbo_frame_tag
# app/views/pages/dashboard.html.erb
<%= turbo_frame_tag "dashboard_chart" do %>
<div class="flex justify-between">
<h3 class="text-lg font-bold">
Q1 Growth
</h3>
<%= link_to current_user.hide_dashboard_chart? ? "Show" : "Hide", toggle_dashboard_chart_path %>
</div>
<% if !current_user.hide_dashboard_chart? %>
<div class="flex justify-center">
<img src="https://quickchart.io/chart?width=400&height=240&chart={type:'bar',data:{labels:['January','February', 'March','April', 'May'], datasets:[{label:'Dogs',data:[50,60,70,180,190]},{label:'Cats',data:[100,200,300,400,500]}]}}" />
</div>
<% end %>
<% end %>
So what's going on here?
We have a turbo_frame_tag
which we've given a name of "dashboard_chart". Inside this frame, there is a top portion with a header and a link. The link shows the words "Show" or "Hide" depending on the value of hide_dashboard_chart
on the current_user
. This link points to our toggle_dashboard_chart_path
, which we'll take a closer look at in a moment.
Below that is the chart itself, for which I'm using a very simple link-based quickchart. If our hide_dashboard_chart
kredis attribute is false, we don't show the chart at all.
We're also using Tailwindcss for some very light styling. It'll look something like this:
Step 3: Update the Kredis attribute in our controller and re-render the turbo frame
We'll need to add a route for the link in our turbo frame
# config/routes.rb
get "toggle_dashboard_chart", to: "pages#toggle_dashboard_chart"
Also, let's go ahead and put the view content from Step 2 in a partial.
# app/views/pages/dashboard.html.erb
<%= render partial: "dashboard_chart" %>
Let's write a helper method on the User model
class User < ApplicationRecord
...
def toggle_hide_dashboard_chart!
hide_dashboard_chart.value = !hide_dashboard_chart.value
end
end
Then put it all together the controller
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
...
def toggle_dashboard_chart
current_user.toggle_hide_dashboard_chart!
render partial: "dashboard_chart"
end
end
Believe it or not, we're done!
Now when we click the link in the Turbo frame, we'll make a round trip to the server where:
- we toggle the
hide_dashboard_chart
Kredis attribute - we return that exact same
dashboard_chart
partial - Only the Turbo frame on our dashboard page updates, this time with the new state, showing/hiding the chart accordingly.
Let's see it in action! 🎥
Takeaways
I've really been enjoying using Hotwire/Turbo, and it presents the opportunity to rethink a lot of ways we handle some common interactions on the web. Hiding/showing some view content by making a round trip to the server might feel strange at first, but I think it actually presents some advantages, notably:
- the code is super simple
- we can easily store/reuse the user's preference after a page refresh
And while we could store these types of preferences in our main database, Redis feels much more appropriate to me. If we somehow lose it, no big deal! And we have total flexibility to change things around later (if our views change) without doing a database migration. Kredis giving us a nicer API to interact with Redis is just icing on the cake.
There are a couple downsides worth mentioning. While the round trip to the server will be imperceptible to most users, those with very slow connections might notice a delay. We also aren't doing any sort of animations to smooth things out here. The chart pops in and out, instantly moving the rest of the page up or down, and some might find that a bit jarring. But overall, I think the pros easily outweigh the cons.
Taking things further
This is fine as a simple proof of concept, but I think for use in production, it could be easily improved in a few obvious ways:
- Replace the "Hide"/"Show" link text with some appropriate icons
- We could certainly be a lot more RESTful, perhaps by making a
POST
request to something likeusers#update
(since after all we are updating a value on the User model)
Thanks so much for reading!
Top comments (2)
The PR for booleans was merged!
Thanks! Updated the article to reflect that. It's still not quite as clean as I was hoping since we can't assign a default value to it (yet, maybe I should open a PR 🤔).