Turbo Streaming Modals in Ruby on Rails
Tags: ruby, rails
Published: 2024-03-27 15:00:00

In part one of this series, we used Hotwire's Stimulus and Turbo Frames to present modals in Rails.

Now, we'll dive into another method we can use to present modals: Turbo Streams.

## What Are Turbo Streams in Ruby on Rails?

[Turbo Streams](https://turbo.hotwired.dev/handbook/streams) is a subset of Turbo. It allows us to make fine-grained, targeted updates to a page. By default, it contains seven CRUD actions, but we're free to add more actions within our applications.

Now, we'll create a `show_remote_modal` action which renders and presents the `<dialog>` from our previous post.

### Creating a Custom Action

Create a folder to place all custom Stream Actions in:

```shell
$ mkdir app/javascript/stream_actions
$ touch app/javascript/stream_actions/index.js
```

And a file for the Action:

```shell
$ touch app/javascript/stream_actions/show_remote_modal.js
```

Import the Stream Actions into the application:

```js
// app/javascript/stream_actions/index.js

import "./show_remote_modal";
```

```js
// app/javascript/application.js

// ...
import "stream_actions";
```

If you're using import maps, you'll need to update the config and restart the server:

```ruby
# config/importmap.rb

# ...
pin_all_from "app/javascript/stream_actions", under: "stream_actions"
```

Change the global remote modal container to an HTML element instead of a Turbo Frame:

```erb
<%# app/views/layouts/application.html.erb %>

<!DOCTYPE html>
<html>
  <%# ... %>

  <body>
    <%= yield %>

    <remote-modal-container></remote-modal-container>
  </body>
</html>
```

The custom Stream Action can be implemented as:

```js
// app/javascript/stream_actions/show_remote_modal.js

Turbo.StreamActions.show_remote_modal = function () {
  const container = document.querySelector("remote-modal-container");
  container.replaceChildren(this.templateContent);
  container.querySelector("dialog").showModal();
};
```

In the above snippet, `this` refers to [`StreamElement`](https://github.com/hotwired/turbo/blob/main/src/elements/stream_element.js), which is the [custom element](https://javascript.info/custom-elements) underpinning `<turbo-stream>`. The `templateContent` getter is defined by this element.

### Using the Action with a Rails Helper

Since this is a custom Action, we'll need to manually create a Rails helper to use it.

```shell
$ bin/rails generate helper TurboStreamActions
```

```ruby
# app/helpers/turbo_stream_actions.rb

module TurboStreamActionsHelper
  def show_remote_modal(&block)
    turbo_stream_action_tag(
      :show_remote_modal,
      template: @view_context.capture(&block)
     )
  end
end

Turbo::Streams::TagBuilder.prepend(TurboStreamActionsHelper)
```

We can now use this helper in our views.

```erb
<%# app/views/support/tickets/new.html.erb %>

<%= turbo_stream.show_remote_modal do %>
  <dialog id="contact_form_modal" aria-labelledby="modal_title">
    <header>
      <h2 id="modal_title">
        Contact us
      </h2>
      <form method="dialog">
        <button aria-label="close">X</button>
      </form>
    </header>

    <%= form_with(url: support_tickets_path) do |form| %>
      <%= form.label :message, "Your message" %>
      <%= form.text_area :message, autofocus: true %>

      <%= form.button "Close", value: nil, formmethod: :dialog %>
      <%= form.button "Send" %>
    <% end %>
  </dialog>
<% end %>
```

Remember to remove the `data-controller` attribute: we don't need it anymore. In fact, we can delete the controller itself.

```shell
$ rm app/javascript/controllers/remote_modal_controller.js
```

We'll also need to change the template's name so it renders as a Turbo Stream.

```shell
$ mv \
    app/views/support/tickets/new.html.erb \
    app/views/support/tickets/new.turbo_stream.erb
```

Turbo Streams are disabled by default for `GET` requests, so we'll need to manually enable them for the link:

```erb
<%# app/views/support/show.html.erb %>

<%# ... %>

<%= link_to new_support_ticket_path, data: { turbo_stream: true } do %>
  Show contact form
<% end %>

<%# ... %>
```

Refresh the page and click _Show contact form_. It should still work as before, but now it's rendered using a custom Stream Action!

## Wrapping Up

In this two-part series, we explored three different methods to present modals using Hotwire: Stimulus, Turbo Frames, and Turbo Streams. More importantly, the modals were presented with accessibility as the main consideration.

The web should be usable by everyone and it's important for us, as web developers, to put in the effort to make websites accessible.

Basecamp's [accessibility guide](https://github.com/basecamp/accessibility) is publicly available and a fantastic resource to learn the ropes.

I also recommend checking out the docs for [Stimulus](https://stimulus.hotwired.dev) and [Turbo](https://turbo.hotwired.dev) to familiarise yourself with all their features and the APIs used in this series.

Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**