Add a “X is writing…” with Rails and Turbo

3D image of a hand with a classic pen, on a wooden table. No paper. The backdrop/background is a window with lush green fields with the sun rising in the back

Apps that rely heavily on text messaging (like Slack, Whatsapp and Telegram) often implement some kind of indication or status if the other party (in the chat room) is typing.

This kind of UX might be useful for engagement or set expectations. Based on your app, seeing the other is typing might keep them around longer. Or it might keep them from adding another message, until you got the message the other was typing.

Luckily for us Rails developers, with the release of Hotwire, this is pretty straightforward to add. Let’s dive in.

I assume you already have some sort of messaging/chat system in place. If you don’t check out the video on hotwired.dev where DHH himself builds a basic chat app.

What is needed?

  1. Tweak the New Message form
  2. Create a Stimulus controller
  3. Create a Rails controller

1. Tweak the New Message form

Add references of the stimulus controller (writing-indicator) to the form element and add an empty div with an id of writing_indicator). This div is where the “X is typing…” text will be inserted.

# app/views/messages/_form.html.erb
<⁠⁠%= form_with(model: message, data: { controller: "writing-indicator", writing_indicator_room_id_value: room.id, writing_indicator_user_id_value: Current.user.id } }) do |form| %>
  <⁠⁠%= form.text_area :content, data: { action: "input->writing-indicator#update" } %>
<⁠⁠% end %>

<div id="writing_indicator">
</div>

2. Create a Stimulus controller

This controller uses two third-party packages @rails/request.js and debounce from lodash.

// app/javascript/controllers/writing_indicator_controller.js
import { Controller } from "@hotwired/stimulus"
import { get } from "@rails/request.js"
import { debounce } from 'lodash"

export default class extends Controller {
  static values = {
    roomId: Number,
    userId: Number,
    debounce: { type: Number, default: 300 }
  }

  initialize() {
    this.update = debounce(this.update.bind(this), this.debounceValue)
  }

  update(event) {
    const url = "/writing_indicator/"

    get(url, {
      query: { user_id: this.userIdValue, room_id: this.roomIdValue },
      responseKind: "turbo-stream"
    })
  }
}

The debounce is to make sure the requests are not fired on every input change in the textarea, but with a slight delay (and reset every time an input falls within the debounceValue number). The update() function is where the magic is happening, by sending a request to the controller’s action created below. It adds both the user_id and the room_id that are both needed in the controller.

3. Create a Rails controller

The update action in this controller broadcasts the app/views/writing_indicators/_update.html.erb partial to the room_#{room_id} channel and replaces the writing_indicator div, created earlier.

# app/controllers/writing_indicators_controller.rb
def update
  room_id = params[:room_id]
  user = User.find(params[:user_id])

Turbo::StreamsChannel.broadcast_replace_to "room_#{room_id}",
    target: "writing_indicator",
    partial: "writing_indicators/update",
    locals: { user: user }
end

Also create a turbo_stream response for the above controller action.

# app/views/writing_indicators/_update.html.erb
<p>
  <⁠%= user.name %> is writing…
</p>

This view assumes the user has a name method. Don’t forget to add a route for above action (resource :writing_indicator) in config/routes.rb. Lastly make sure you use the correct channel to broadcast too (eg. turbo_stream_from @room`), you likely already have something like this in place for the chat functionality.

Next steps

While these steps will add the basics of this kind of UX to your app, there are more things to consider:

  • what should happen on submit?
  • what should happen after typing and then closing the screen?

I’ll leave that up to you, but feel free to reach out if you get stuck on anything with the above points.

And that is really all you need to get a “X is writing…” UX in your Rails chat app. Mind-blown already? Of course it needs some UI love, but I leave that up you or you can check out Rails Designer. It’s the first professionally-designed UI components library for Rails. Built with ViewComponent, designed with Tailwind CSS and enhanced with Hotwire.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire