Toggling page content with Turbo Frames and Kredis
Tags: rails, hotwire, redis
Published: 2022-01-14 14:51:03

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](https://www.w3schools.com/howto/howto_js_accordion.asp). 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](https://turbo.hotwired.dev/handbook/frames) and the newly released gem [Kredis](https://github.com/rails/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:
1. Use Kredis to add a Redis-backed attribute on our User model to store the state
2. Wrap the content in a `turbo_frame_tag`, including a link that will function as a hide/show button
3. 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](https://github.com/rails/kredis#installation), we're ready to add the Kredis attribute to our User model.

```ruby
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`

```erb
# 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](https://quickchart.io). 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:
![screenshot of the page with a chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p3nh4y32ipnreojghv5.png)
 
## 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
```ruby
# 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. 

```erb
# app/views/pages/dashboard.html.erb

<%= render partial: "dashboard_chart" %>

```

Let's write a helper method on the User model

```ruby
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

```ruby
# 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! 🎥
<img width="100%" style="width:100%" src="https://media.giphy.com/media/TDGgDqtwaUqEw9qcp7/giphy.gif" />

<br/>

## 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 like `users#update` (since after all we are updating a value on the User model)

Thanks so much for reading!