In this series we are building an admin panel, using Hotwire. The admin panel will have multiple tabs and plenty of interactivity. Let’s get started.
What is Hotwire?
Hotwire is a new framework, by Basecamp, which is built into Rails 7. The framework provides an alternative to frontend frameworks like React.
Frontend architecture typically involves a JSON API along with large amounts of frontend JavaScript code. Instead, Hotwire sends HTML fragments from the server, which are rendered on the frontend with minimal JavaScript.
Building an admin dashboard
Here’s a mockup of the admin panel we’re building:
There’s a row of tabs that switch between various business metrics. Each of the metrics will be updated in real time.
Building the layout
First we’ll build the page layout to roughly match the mockup. We’ll start by adding a route.
1
root 'home#index'
We’ll also need a controller and index action.
1
2
3
4
class HomeController < ApplicationController
def index
end
end
Next, we’ll add the HTML layout. The design doesn’t need to be perfect at this stage, it just needs to include the key elements.
1
2
3
4
5
6
7
8
<h1>Demo Admin Dashboard</h1>
<div class="button-row">
<button>Real time revenue</button>
<button>Real time orders</button>
</div>
<h2>$104,000</h2>
Lastly, we’ll add a little CSS to center everything:
1
2
3
4
5
6
7
h1, h2 {
text-align: center;
}
.button-row {
text-align: center;
}
Making revenue dynamic
In this part 1 post, we’re going to focus on making revenue dynamic. In order to do that we’re going to leverage Turbo Streams, which allows Rails to deliver page changes, as HTML fragments. Whenever a sale is recorded in the database, it should be reflected on the UI, in realtime.
Adding a model to capture sales
Before we can start adding Turbo Streams functionality, we’ll need a Sale
model to record sales. Let’s add that using the Rails generator:
1
bin/rails g model Sale amount:integer
For this tutorial Sale
just needs an amount field.
We also need a method for calculating the sales total, which will be used to display the revenue on the page. We can do that with a simple class method.
1
2
3
def self.total
pluck(:amount).sum
end
Adding view logic to update revenue
With the model in place, we can go ahead and replace the hardcoded amount. In order to make it dynamic, we can use Turbo’s turbo_stream_from
method.
1
2
3
4
<%= turbo_stream_from "revenue" %>
<div id="revenue">
<%= render partial: 'metrics/revenue', locals: { revenue: @revenue } %>
</div>
turbo_stream_from
is used to subscribe to a stream, in this case, a stream called revenue
.
On the next line, there is a div that wraps the partial that renders the revenue number. Turbo streams will dynamically re-render this number as long as the message fragment targets revenue
. We’ll cover that in the next section.
Lastly, we need to create the partial, under metrics/_revenue.html.erb
.
1
2
3
<div class="revenue">
<%= number_to_currency(revenue) %>
</div>
Broadcasting sales from the model
The last thing we need to do is broadcast messages to the view. Turbo Streams can broadcast directly from models, via callbacks, using broadcast_update_to
.
1
after_commit -> { broadcast_update_to "revenue", partial: "metrics/revenue", locals: { revenue: Sale.total }, target: "revenue" }
In this case, we’re broadcasting to the revenue
stream, which is the same one we’re subscribed to in the view. We’re also telling broadcast_update_to
to render the metrics/revenue
partial, as well as passing in the required local variables. Lastly, we’re specifying the target as revenue
, which will target the id="revenue"
element in the view. This tells Turbo which element to update.
Testing
We can test this system via the Rails console. But first, we need to make sure Redis is running. On a mac you can install it via brew and run it using brew services
.
To test, simply run the Rails console and create a new Sale
:
1
2
3
$ bin/rails c
Loading development environment (Rails 7.0.2.4)
irb(main):001:0> Sale.create!(amount: 21)
We should see the revenue number updated in realtime.