Hotwire Discussion

Triggering Turbo Frame with JS

Regarding the hidden button approach, I initially did it as a test to better understand what was working and what wasn’t so it was more of a starting point, very hacky. I agree that using it in production would be a bad idea. For what I was trying to figure out (submitting a form with JS while still taking advantage of either Frames or Streams), requestSubmit() was exactly what I was looking for.

I am still digging through the source cause I find it very interesting. I’ll share anything that may be relevant. Please do the same, I’d be interesting in hearing what you figure out.

1 Like

I’m stuck on this same problem. And for some reason I can’t find the method requestSubmit. It doesn’t even exist in the hotwired/turbo repo. Where can I find it?

It’s a standard function that is implemented on form elements in JavaScript. In order to access the method you’ll need to call it on the form element that you’re trying to submit.

1 Like

It’s part of the HTML spec, built in to any browser: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit

Walter

Here’s the interesting part of that documentation:

The obvious question is: Why does this method exist, when we’ve had the submit() method since the dawn of time?

The answer is simple. submit() submits the form, but that’s all it does. requestSubmit(), on the other hand, acts as if a submit button were clicked. The form’s content is validated, and the form is submitted only if validation succeeds. Once the form has been submitted, the submit event is sent back to the form object.

Walter

4 Likes

Props to @jacobdaddario for requestSubmit.

I’ve successfully replaced the following rails-ujs code with Turbo.

Before:

<%= check_box_tag dom_id(todo, "checkbox"), 1, todo.completed?, data: { remote: true, url: toggle_todo_path(todo), method: :post } %>

After:

<%= form_with model: todo, url: toggle_todo_path(todo), method: :post do |form| %>
  <%= form.check_box :completed, data: { controller: "checkbox", action: "checkbox#submit" } %>
<% end %>
// app/javascript/controllers/checkbox_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  submit() {
    this.element.closest("form").requestSubmit();
  }
}

You can see the whole commit here: https://github.com/mrhead/todos/pull/49/commits/0dc609973e7d7310e59506c05bb4101a0be8b025

1 Like

Remember that Safari on all platforms does not support this. You’ll need to add a polyfill to support your mobile users.

Walter

4 Likes

Would it be more compatible to manually fire the submit event on the form? I believe in the other thread that was a solution proposed by another commenter.

This is a bit out of topic, but I would like to mention it anyway.

Because of out of order responses, I would suggest to use Stimulus and manually issue and abort fetch requests. I.e. if we need to send a new fetch request and the previous one did not finish yet, then just abort the previous one. This fixes out of order search results.

You can see it in my Stimulus search example here: https://github.com/mrhead/stimulus-search/blob/main/app/javascript/controllers/search_controller.js

Back to the original problem. Yesterday I thought that requestSubmit() is the right answer. However, as it was pointed out by @walterdavis, it’s not supported by Safari.

So I tried to manually fire the submit event:

this.element.closest("form").dispatchEvent(new CustomEvent("submit", { bubbles: true }))

This works in Safari and Chrome, but not in Firefox which just submits the form without Turbo :man_shrugging:.

So if anyone is using the custom event approach, then I would suggest to test it in Firefox.

In the meantime, I’m going to look for a solution which work in all mainstream browsers :).

I’ve decided to use form-request-submit-polyfill which is pretty lightweight and works well for me.

1 Like

And coming full circle, if you dig into how it works, it creates a hidden submit button and “clicks” it!

Walter

3 Likes

So… I might have found something.

This might be a private API, but we can use Turbo’s Navigator class to submit forms.

Here is an example:

import { Controller } from "stimulus"
import { navigator } from "@hotwired/turbo"

export default class extends Controller {
  submit() {
    navigator.submitForm(this.element.closest("form"))
  }
}

The issue is that you need to use the npm package and as far as I know you can’t use this if you use turbo_include_tags from turbo-rails.

You can see it in this experimental pull request: https://github.com/mrhead/todos/pull/58/files

2 Likes

For what it’s worth, I’ve kept rails-ujs to submit forms for now. I’m using it like this:

Rails.fire(form, "submit")

Sure enough, for a form (without a submit button) that’s in a Turbo Frame this does trigger turbo requests.

Now there has been some discussion about keeping rails-ujs, but I’m not sure if this particular function should be on the list (@dhh?). And I’m not even sure if I’m abusing stuff or if Rails.fire is indeed a sure-fire way of getting this to work.

2 Likes

requestSubmit works!

simmilar to your solution, in mine I’m using target

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [form]
  submit() {
    formTarget.requestSubmit();
  }
}


<%= form_with model: todo, url: toggle_todo_path(todo), method: :post, data: {target: "checkbox.form"} do |form| %>
  <%= form.check_box :completed, data: { controller: "checkbox", action: "checkbox#submit" } %>
<% end %>

(something like this, I’m writing from top of my head, but you get the picture :slight_smile: )

So, can anybody confirm whether requestSubmit() also works for Turbo streams too?

That would solve @danjac’s problem:

one problem I have is sending updates generated from HTML media element events, so using a form submission is not really an option. Of course I use fetch(); but then I’m handling low level HTML changes in Stimulus. It works, but it could be a lot more elegant if I could use streams.

UPDATE:

I can now confirm that requestSubmit() successfully works with Turbo streams as well.

I’m interested in this also. However, I think it was established that requestSubmit isn’t supported in Safari

1 Like

There seems to be a polyfill for this @MrHubble which should do the trick for now.

Alternatively, you could use a GET response and dynamically update the src of the turbo-frame with a GET query. No polyfill needed.

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ['results'];
  static values = {url: String}

  findResults(event) {
    this.resultsTarget.src = this.urlValue.replace(/%s/g, encodeURIComponent(event.target.value))
  }
}
<div data-controller="search" data-search-url-value="books?q=%s">
  Search:
  <input type="search" data-action="search#findResults">
  <turbo-frame id="search-results" data-search-target="results"></turbo-frame>
</div>
5 Likes

I like this approach a lot. I realized you could use src in this manner a while after I wrote my original response, but I think this may be better. My only concern would be that it kind of gets into the guts of how frames work.