Building a dynamic dashboard with Hotwire, part 2

In part 1 we built an admin panel and used Turbo Streams to make revenue dynamic. In this post we’ll continue building out the admin panel. We’ll implement tabs, using Turbo Frames, then finish up by making ‘real time orders’ dynamic.

Turbo Frame tabs

First, we’ll update the existing logic for displaying revenue. We’ll wrap it in a turbo_frame_tag block and call it "tab_data". This tag is what Turbo will use to replace the content when changing tabs. All we need to do is render a new "tab_data" frame, from a controller.

Also, we’ll update the partial to make it more generic:

index.html.erb

1
2
3
4
5
6
<%= turbo_frame_tag "tab_data" do %>
  <%= turbo_stream_from "revenue" %>
  <div id="data">
    <%= render partial: 'metrics/financial_data', locals: { data: @revenue } %>
  </div>
<% end %>

metrics/_financial_data.html.erb

1
2
3
<div id="data">
  <%= number_to_currency(data) %>
</div>

In part 1 we created stub buttons to control the tabs. To get them working we need to add new routes:

1
2
3
4
5
6
  resource :dashboard do
    member do
      get :revenue
      get :orders
    end
  end

With the routes created we can update the button links:

1
2
<%= button_to "Real time revenue", revenue_dashboard_path, method: :get %>
<%= button_to "Real time orders", orders_dashboard_path, method: :get %>

These links need a controller, we can generate one with an action for ‘revenue’ and ‘orders’:

1
bin/rails g controller DashboardsController revenue orders

In the controller actions we ultimately want to render a "tab_data" Turbo Frame, just like the one on the index page. We’ll place the Turbo Frame code inside a partial and render that from the controller. We’ll also use turbo_frame_request? to make sure this partial is only rendered for Turbo Frame requests.

The rendering logic for each tab is similar but different enough to justify two different partials. The partials will contain the Turbo Frame and Turbo Stream code. The controller just needs to pass in the data to display:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  def revenue
    if turbo_frame_request?
      respond_to do |format|
        format.html { render partial: 'dashboards/revenue_tab',
          locals: { data: Sale.total }}
      end
    else
      # do something else
    end
  end

  def orders
    if turbo_frame_request?
      respond_to do |format|
        format.html { render partial: 'dashboards/orders_tab',
          locals: { data: Sale.count }}
      end
    else
      # do something else
    end
  end

Next we need to build the partials. The outer block creates a Turbo Frame tag, which provides the tab behaviour. The inner block defines a Turbo Stream subscription, which allows the metrics to be updated in realtime:

revenue_tab.html.erb

1
2
3
4
5
6
<%= turbo_frame_tag "tab_data" do %>
  <%= turbo_stream_from "revenue" %>
  <div id="revenue">
    <%= number_to_currency(data) %>
  </div>
<% end %>

orders_tab.html.erb

1
2
3
4
5
6
<%= turbo_frame_tag "tab_data" do %>
  <%= turbo_stream_from "orders" %>
  <div id="orders">
    <%= data %>
  </div>
<% end %>

The Turbo Frames implementation is now working end to end. Turbo will handle all the magic of updating the tabs.

Getting real time orders working

There’s one last change required to complete the dynamic dashboard. In part 1 we got ‘revenue’ working but didn’t complete ‘orders’. To fix it, we need to add an additional after_commit that broadcasts order updates. We’ll use Sale.count to represent orders:

sale.rb

1
after_commit -> { broadcast_update_to "orders", partial: "metrics/data", locals: { data: Sale.count }, target: "orders" }

Lastly, we need to add a small partial containing the code block we’re broadcasting:

metrics/_data.html.erb

1
2
3
<div id="data">
  <%= data %>
</div>

That’s it! Thank you for reading. We hope you enjoying this 2 part Hotwire series, covering Turbo Streams and Turbo Frames.