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:
$ mkdir app/javascript/stream_actions $ touch app/javascript/stream_actions/index.js
And a file for the Action:
$ touch app/javascript/stream_actions/show_remote_modal.js
Import the Stream Actions into the application:
// app/javascript/stream_actions/index.js import "./show_remote_modal";
// app/javascript/application.js // ... import "stream_actions";
If you're using import maps, you'll need to update the config and restart the server:
# 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:
<%# 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:
// 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.
$ bin/rails generate helper TurboStreamActions
# 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.
<%# 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.
$ 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.
$ 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:
<%# 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!