ruby

Turbo Streaming Modals in Ruby on Rails

Ayush Newatia

Ayush Newatia on

Turbo Streaming Modals in Ruby on Rails

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 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:

javascript
// app/javascript/stream_actions/index.js import "./show_remote_modal";
javascript
// 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:

javascript
// 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, which is the custom element 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 is publicly available and a fantastic resource to learn the ropes.

I also recommend checking out the docs for Stimulus and Turbo 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!

Ayush Newatia

Ayush Newatia

Guest author Ayush is a freelance Ruby and Rails developer. He's the author of The Rails and Hotwire Codex and part of the Bridgetown core team. He also runs a privacy focused mailing list app called Scattergun.

All articles by Ayush Newatia

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps