Turbo Streaming Modals in Ruby on Rails

ayushn21

Ayush Newatia

Posted on March 27, 2024

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:

$ mkdir app/javascript/stream_actions
$ touch app/javascript/stream_actions/index.js
Enter fullscreen mode Exit fullscreen mode

And a file for the Action:

$ touch app/javascript/stream_actions/show_remote_modal.js
Enter fullscreen mode Exit fullscreen mode

Import the Stream Actions into the application:

// app/javascript/stream_actions/index.js

import "./show_remote_modal";
Enter fullscreen mode Exit fullscreen mode
// app/javascript/application.js

// ...
import "stream_actions";
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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)
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

<%# ... %>
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
ayushn21
Ayush Newatia

Posted on March 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related