Building a Real Time Scoreboard with Ruby on Rails and CableReady
Tags: ruby, rails, webdev, tutorial
Published: 2021-08-18 23:20:11

The release of Hotwire in late 2020 brought attention to a growing interest within the Rails community in building modern, [reactive](https://cableready.stimulusreflex.com/advocating-for-reactive-rails) Rails applications without needing the complexity of an API + SPA.

Although Hotwire's [Turbo](https://turbo.hotwired.dev/) library garnered a lot of attention, the Rails community has been working for years to improve the toolset we have to build modern, full-stack Rails applications. Turbo isn't the first attempt at giving Rails developers the tools they need.

One of the most important of these projects is [CableReady](https://cableready.stimulusreflex.com/), which powers [StimulusReflex](https://docs.stimulusreflex.com/) and [Optimism](https://optimism.leastbad.com/), along with standing on its own as a tool to:

> Create great real-time user experiences by triggering client-side DOM changes, events and notifications over [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) web sockets ([source](https://cableready.stimulusreflex.com/))

Today we're going to explore CableReady by using Rails, CableReady, and [Stimulus](https://stimulus.hotwired.dev/) to build a scoreboard that updates for viewers in real-time, with just a few lines of Ruby and JavaScript.

When we’re finished, our scoreboard will look like this:

![A screen recording of a user with a web page and a terminal window open. On the page are the scores for two teams, Miami and Dallas. The user types a command in the terminal to update the home team's score to 95 and, after the command runs, the score on the web page for Miami updates to 95.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsn398zh2u375v7vtbxh.gif)

This article assumes that you're comfortable working with Rails but you won't need any prior knowledge of CableReady or ActionCable to follow along. If you've never used Rails before, this article isn't the best place to start.

Let’s dive in!

## Application Setup
First, let’s create our Rails application, pull in CableReady and Stimulus, and scaffold up a Game model that we’ll use to power our scoreboard.

```shell
rails new scoreboard_ready -T
cd scoreboard_ready
bundle add cable_ready
yarn add cable_ready
rails webpacker:install:stimulus
rails g scaffold Game home_team:string away_team:string home_team_score:integer away_team_score:integer
rails db:migrate
```

Although Redis is not technically required in development for CableReady, we’ll use it and [hiredis](https://github.com/redis/hiredis-rb) to match the [installation guidance](https://cableready.stimulusreflex.com/installation) from CableReady. 

Update your Gemfile with these gems:

```ruby
gem 'redis', '~> 4.0' # Uncomment this line, it should already be in your Gemfile
gem 'hiredis'
```

And then `bundle install` from your terminal.

Don’t have Redis installed in your development environment? Installing Redis on your machine is outside the scope of this article, but you can find instructions for [Linux](https://redis.io/topics/quickstart) and [OSX](https://phoenixnap.com/kb/install-redis-on-mac) online.

Finally, we need to update our ActionCable configuration in config/cable.yml to use Redis in development:

```yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: scoreboard_ready_development

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: scoreboard_ready_production
```

With the application setup complete, we’ll build the basic layout for our scoreboard next.

## Setup the scoreboard view
First, update the games show view:

```erb
<div style="text-align: center;">
  <div style="margin-top: 2rem;">
    <%= link_to 'Back to all games', games_path %>
  </div>
  <%= render "game_detail", game: @game %>
</div>
```

Here we’re inlining a couple of styles to make the scoreboard a little more legible and rendering a `game_detail` partial that doesn’t exist yet. Add that next, from your terminal:

```shell
touch app/views/games/_game_detail.html.erb
```

And fill it in with:

```erb
<div>
  <h1><%= "#{game.home_team} vs. #{game.away_team}" %></h1>
  <div style="color: gray; font-size: 1.2rem; margin-bottom: 0.8rem;">
    <%= "#{game.home_team}: #{game.home_team_score}" %>
  </div>
  <div style="color: gray; font-size: 1.2rem;">
    <%= "#{game.away_team}: #{game.away_team_score}" %>
  </div>
</div>
```

Some more inline styles (we’ll remove these later!) with some standard erb to render each team’s name and score.

At this point, we can go to [localhost:3000/games](http://localhost:3000/games) and create a game, and then go to the game show page to view it. 

We don't have real-time updates in place yet, we'll start building that with CableReady next.

## Create channel and controller
Our first step to delivering real-time updates is to add a channel to broadcast updates on. When a user visits a game’s show page, they’ll be subscribed via a WebSocket connection to an ActionCable channel. 

Without a channel subscription, the CableReady broadcasts we’ll be sending soon won’t be received.

To create a channel we can use the built-in generator:

```shell
rails g channel Game
```

This will create a few files for us. For the purposes of this article, we’re interested in the `game_channel.rb` file created in `app/channels`.

Open that file and update the subscribed method:

```ruby
def subscribed
  stream_or_reject_for Game.find_by(id: params[:id])
end
```

The `subscribed` method is called each time a new [Consumer](https://guides.rubyonrails.org/action_cable_overview.html#consumers) connects to the channel. 

In this method, we’re using the [ActionCable method](https://api.rubyonrails.org/v6.1.0/classes/ActionCable/Channel/Streams.html#method-i-stream_or_reject_for) `stream_or_reject_for` to create a [Stream](https://guides.rubyonrails.org/action_cable_overview.html#streams) that will send subscribed users broadcasts for a specific instance of a game, based on an `id` parameter.

When no game is found, the subscription request will be rejected.

With the channel built, next we need to allow consumers to subscribe to the channel so they can receive broadcasted updates.

The channel generator we ran automatically creates a file at `javascripts/channels/game_channel.js` that we could use to handle the subscription on the frontend; however, CableReady really shines when [combined](https://cableready.stimulusreflex.com/leveraging-stimulus) with Stimulus.

To do that, we’ll create a new Stimulus controller, from the terminal:

```shell
touch app/javascript/controllers/game_controller.js
```

And fill it in with:

```javascript
import { Controller } from 'stimulus'
import CableReady from 'cable_ready'

export default class extends Controller {
  static values = { id: Number }

  connect() {
    this.channel = this.application.consumer.subscriptions.create(
      {
        channel: 'GameChannel',
        id: this.idValue
      },
      {
        received (data) { if (data.cableReady) CableReady.perform(data.operations) }
      }
    )
  }
  
  disconnect () {
    this.channel.unsubscribe()
  }
}
```

This Stimulus controller is very close to `game_channel.js` created by the channel generator, with a little Stimulus and CableReady power added.

Each time the Stimulus controller connects to the DOM, we create a new consumer subscription to the `GameChannel`, passing an `id` parameter. When the Stimulus controller disconnects from the DOM, the subscription is removed.

When a broadcast is received by the consumer, we use CableReady to `perform` the requested [operations](https://cableready.stimulusreflex.com/reference/operations).

Before the Stimulus controller will work, we need to update `app/javascript/controllers/index.js` to import `consumer.js` (part of the `ActionCable` package) and attach consumer to the Stimulus Application object. 

Update `controllers/index.js` with these two lines of code to accomplish that:

```javascript
import consumer from '../channels/consumer'
application.consumer = consumer
```

Read more about why this is the right way to combine ActionCable and Stimulus [here](https://cableready.stimulusreflex.com/leveraging-stimulus#1-this-application-consumer).

With our Stimulus controller built, we can update the `game_detail` partial to connect the controller to the DOM.

```erb
<div 
  id="<%= dom_id(game) %>"
  data-controller="game"
  data-game-id-value="<%= game.id %>"
>
  <h1><%= "#{game.home_team} vs. #{game.away_team}" %></h1>
  <div style="color: gray; font-size: 1.2rem; margin-bottom: 0.8rem;"><%= "#{game.home_team}: #{game.home_team_score}" %></div>
  <div style="color: gray; font-size: 1.2rem;"><%= "#{game.away_team}: #{game.away_team_score}" %></div>
</div>
```

Here we accomplished a lot with one change to the parent div:
* We attached the Stimulus controller to the parent div
* Set the id [value](https://stimulus.hotwired.dev/reference/values) that the Stimulus controller uses to send the id param in the channel subscription request
* Set the id of the div to the dom_id of the rendered game instance. We’ll use this id in the CableReady broadcast we’ll generate in our model, up next.

With all of this in place, visit a game show page and check the Rails server logs. If everything is setup correctly, you should see log entries that look like this after the show page renders:

```
GameChannel is transmitting the subscription confirmation
GameChannel is streaming from game:Z2lkOi8vc2NvcmVib2FyZC1yZWFkeS9HYW1lLzE
```

## Broadcast game updates from the model
With the channel built and consumers subscribing to updates, our last step to real-time scoreboard updates is sending a broadcast each time a game is updated.

The simplest way to do this is to broadcast a CableReady operation in an `after_update` callback in the `Game` model. 

To make this possible, we first need to include the CableReady `Broadcaster` in our models and delegate calls to `render` to the `ApplicationController`, as described in the (excellent) CableReady [documentation](https://cableready.stimulusreflex.com/cableready-everywhere#activerecord).

Update `app/models/application_record.rb` as follows:

```ruby
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  include CableReady::Broadcaster
  delegate :render, to: :ApplicationController
end
```

And then update `app/models/game.rb`:

```ruby
class Game < ApplicationRecord
  after_update do
    cable_ready[GameChannel].morph(
      selector: dom_id(self),
      html: render(partial: "games/game_detail", locals: { game: self})
    ).broadcast_to(self)
  end
end
```

Here we’ve added an `after_update` callback to trigger a CableReady `broadcast`. The broadcast is sent on the `GameChannel`, queuing up a `morph` [operation](https://cableready.stimulusreflex.com/reference/operations/dom-mutations#morph) targeting the current game instance, and rendering the existing `game_detail` partial.

With this callback in place, our scoreboard should now update in real-time. 

You can test this yourself by heading to a game show page and then opening your Rails console and running something like `Game.find(some_id).update(home_team_score: 100)`.

You should see the score update in the browser window immediately after submitting the update command in the Rails console.

While this works pretty well, our scoreboard really only needs to receive updates when the score changes, and it would be helpful to provide a little feedback to the user when the score changes. 

Let’s finish up this article by updating our implementation to broadcast only on score changes, and to animate newly updated scores.

## Getting fancier
To start, we’ve got some clunky inline styling that makes our erb code pretty hard to follow. Let’s move those styles out of the HTML and into a stylesheet. From your terminal:

```shell
mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/application.scss
```

And add the below to `application.scss`:

```scss
.score-container {
  display: flex;
  color: gray;
  font-size: 1.2rem;
  margin-bottom: 0.8rem;
  justify-content: center;
}
.swing-in-top-fwd {
  -webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
          animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
}
@-webkit-keyframes swing-in-top-fwd {
  0% {
    -webkit-transform: rotateX(-100deg);
            transform: rotateX(-100deg);
    -webkit-transform-origin: top;
            transform-origin: top;
    opacity: 0;
  }
  100% {
    -webkit-transform: rotateX(0deg);
            transform: rotateX(0deg);
    -webkit-transform-origin: top;
            transform-origin: top;
    opacity: 1;
  }
}
@keyframes swing-in-top-fwd {
  0% {
    -webkit-transform: rotateX(-100deg);
            transform: rotateX(-100deg);
    -webkit-transform-origin: top;
            transform-origin: top;
    opacity: 0;
  }
  100% {
    -webkit-transform: rotateX(0deg);
            transform: rotateX(0deg);
    -webkit-transform-origin: top;
            transform-origin: top;
    opacity: 1;
  }
}
```


To animate the scores, we’re just using a simple CSS swing animation, copy/pasted directly from the always handy [Animista](https://animista.net/play/entrances/swing-in).

Finally import that new stylesheet into the webpack bundle:

```javascript
// app/javascripts/application.js
import "stylesheets/application"
```

We want to be able to update scores individually. To enable that, we’ll move the score portion of the scoreboard into a dedicated partial that we can then render in a broadcast.

From your terminal:

```shell
touch app/views/games/_score.html.erb
```

And fill that in with:

```erb
<div id="<%= "#{team}_score" %>" class="swing-in-top-fwd">
  <%= score %>
</div>
```

Then update the `game_detail` partial to remove the inline styles and to use our new `score` partial:

```erb
<div data-controller="game" 
  data-game-id-value="<%= game.id %>" 
  id="<%= dom_id(game) %>"
>
  <h1><%= "#{game.home_team} vs. #{game.away_team}" %></h1>
  <div class="score-container">
    <div>
      <%= game.home_team %>:
    </div>
    <%= render "score", 
      score: game.home_team_score, 
      team: "home" 
    %>
  </div>
  <div class="score-container">
    <div>
      <%= game.away_team %>:
    </div>
    <%= render "score", 
      score: game.away_team_score, 
      team: "away" 
    %>
  </div>
</div>
```

Finally, we’ll update the callback in the `Game` model:

```ruby
class Game < ApplicationRecord
  after_update_commit { broadcast_changes }

  def broadcast_changes
    update_score(team: 'home') if saved_change_to_home_team_score? 
    update_score(team: 'away') if saved_change_to_away_team_score?
  end

  def update_score(team:)
    cable_ready[GameChannel].outer_html(
      selector: "##{team}_score",
      html: render(partial: 'games/score', locals: { score: send("#{team}_team_score"), team: team })
    ).broadcast_to(self)
  end
end
```

Here we’ve updated our callback to check for changes to the two attributes we care about (`home_team_score` and `away_team_score`). When either attribute is changed, a broadcast is triggered from `update_score` to replace the target div’s contents with the content of the `score` partial. 

We use the `outer_html` CableReady operation in this case to completely replace the DOM content and ensure that our animation triggers when the new score content enters the DOM.

And with that in place, we can now see our isolated, animated real-time updates:

![A screen recording of a user with a web page and a terminal window open. On the page are the scores for two teams, Miami and Dallas. The user types a command in the terminal to update the home team's score to 95 and, after the command runs, the score on the web page for Miami updates to 95.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsn398zh2u375v7vtbxh.gif)

## Wrapping up
Today we explored how to add CableReady and Stimulus onto the core Rails ActionCable package to enable real-time DOM manipulations without writing tons of JavaScript, worrying about client-side state management, or doing much outside of writing pretty standard Rails code. The complete source code for this demo application is on [Github](https://github.com/DavidColby/scoreboard_ready).

CableReady (and StimulusReflex, which we’ll explore in a future article) are mature, powerful tools that allow Rails developers to create modern, reactive applications while keeping development time and code complexity low. CableReady also plays well with most of Turbo, and can fit seamlessly into a Hotwire-powered application.

Start your journey deeper into CableReady and reactive Rails applications with these resources:
* Read the [CableReady documentation](https://cableready.stimulusreflex.com/)
* Read the [StimulusReflex documentation](https://docs.stimulusreflex.com/)
* Read the [Hotwire documentation](https://hotwired.dev/)
* Talk to the helpful folks on the [StimulusReflex discord](https://discord.gg/stimulus-reflex)

As always, thanks for reading!