Triggering Turbo Frame with JS

Hey! So it looks like @mgodwin beat me to a punch on a couple of things! First things first, I uploaded a test project with this functionality to my GitHub account. You can find it here. Even if this isn’t useful now, I wanted to drop a link to it for anyone who may stumble across this Discourse thread in the future.

That said, @mgodwin already nailed the tricky part. It turns out that the turbo-stream mechanism listens for form submission events, and for some reason the submit() function does not emit a form submission event. That means that it’ll bring back a normal HTML response. That said, it looks like there’s another method, requestSubmit() which does issue a submit event. Weird stuff from JavaScript land. Consequently, you can write your controller and html in the following fashion:

<!-- app/views/books/index.html.erb -->
<%= form_tag('/search', method: :get, data: { controller: "search" }) do %>
  <%= text_field_tag :search, params[:search], data: { action: "input->search#findResults" } %>
<% end %>
<div id="books"></div>
// app/assets/javascripts/controllers/search_controller.js

import { Controller } from "stimulus"

export default class extends Controller {
  findResults(event) {
    var searchValue = event.target.value;

    this.element.requestSubmit();
  }
}
# app/controllers/search_controller.rb
class SearchController < ApplicationController
  def search
    @books = Book.search_title(params[:search])

    respond_to do |format|
      format.turbo_stream
      format.html         { redirect_to root_url(search: params[:search]) }
    end
  end
end
<!-- app/views/search/search.turbo_stream.erb -->
<turbo-stream action="update" target="books">
  <template>
    <ul>
      <% @books.each do |book| %>
        <%= content_tag :li, book.title %>
      <% end %>
    </ul>
  </template>
</turbo-stream>

Like @mgodwin mentioned, you could add a debounce function like is mentioned in this thread. That said, I discovered a couple of interesting things while working on this toy app that I wanted to document here.

First, controllers being moved into the assets directory makes it harder to add packages like lodash to help with utility functions like debounce. Yarn package management seems to still be tied to Webpack, which is kind of frustrating. I wonder if there’s a particular reason that the controllers have been moved back into the asset folder in the stimulus-rails integration? If the asset pipeline had access to JavaScript package management, this would probably be a welcomed change by many.

Next, I originally tried to erroneously trigger a turbo-stream response using fetch() to send a get request to the server. As we’ve already learned, Turbo listens purely for the submit event, meaning it requires a form to be submitted. That said, when I was using this approach it surprisingly was still returning turbo-stream fragments to the server, it just wasn’t injecting them. I imagine the behavior of Turbo just listening for submit events is intentional in order to retain the spirit of semantic HTML and progressive enhancement. That said, I’m curious as to what causes a fetch request to return a turbo-stream response.

Finally to reply to @danjac-2020 , I think the big thing with turbo-streams is to provide a standard approach to sending HTML fragments over the wire to be injected into the DOM. I’ve done some toying around with HTML fragments over the wire just using fetch requests and Stimulus prior to the release of Turbo, and I ran into two big problems.

  1. Inserting HTML outside of a Stimulus controller gets kind of hacky. You can’t assign something outside of a controller as a target, which means you need to use a typical getElementById() call. This totally works, but it feels out of place in a Stimulus controller which has the nicely organized target system.

  2. Adding multiple fragments of HTML to the page in response to one request is really hard. Or at least in my experiments it was a struggle. Let’s say that you have the user submitting a form, and on a validation error you want to inject inline errors and a toast message into the DOM. With a normal fetch request, the server’s response comes back as a single HTML file that you have to break up into parts and inject into the page. With turbo-streams, you can inject multiple fragments into different parts of the page in a single request, with no added overhead on your part. All you have to do is ping the server with a form submission and have a turbo-stream response with multiple fragments.

Turbo seems to be the logical conclusion of the original Turbolinks. The original idea is still there as turbo-drive, but now there is also context-specific navigation using turbo-frames and a standard way to inject HTML into an existing DOM using turbo-drive. Additionally, it avoids dependence on websockets unlike a solution like StimulusReflex. I really enjoyed working with it today, and if there’s any way that we could contribute to the docs, I’d love to help flesh them out.

3 Likes