20

I have a form I want to submit automatically whenever any input field is changed. I am using Turbo Streams, and if I use onchange: "this.form.submit()" it isn't captured by Turbo Streams and Rails uses a standard HTML response. It works fine when clicking the submit button. How can I work around this?

tsvallender
  • 2,615
  • 6
  • 32
  • 42

2 Answers2

53

There is a discussion on the hotwire forum, where Mark Godwin figured out why form.submit() isn't working with turbo:

Turbo intercepts form submission events, but weirdly, the JS formElement.submit() method does not trigger the submit event.

And Jacob Daddario figures out that you can use form.requestSubmit() instead:

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.

So you can change your code slightly, and use requestSubmit() if a browser supports it, and use submit() if not:

onchange: "this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()"


Update:

As BenKoshy pointed out, in Turbo 7.1.0, a polyfill was added so you can use form.requestSubmit() without checking for browser support, so you can add this to your input field:

onchange: "this.form.requestSubmit()"
wnm
  • 1,349
  • 13
  • 12
  • 7
    I confirm this works. – sparkle Oct 26 '21 at 10:52
  • 2
    There's a polyfill which handles the browser compatibility issue out of the box, so long as you install the latest version of turbo, you'll be fine. – BenKoshy Nov 25 '21 at 05:38
  • this works if you posting form. but if you have form_with( ... method: :get ) it will be submitted in html format. to fix this behavior you can use form_with(... data: { turbo_stream: true }). This is for turbo-rails 1.4.0 which is latest a.t.m. – Volodymyr Aug 10 '23 at 16:08
  • There's nothing weird about form.submit() not emitting the event. A standard pattern is the "submit" event is listened for, something done, then the form submitted from within the event handler. While it's possible to mitigate, the standard functionality would be an infinite loop. – Michael Chaney Sep 01 '23 at 15:09
3

I need to implement this for an app with lots of forms. I wound up using Stimulus. Below is the whole controller:

import { Controller } from "stimulus"
const _ = require("lodash")
export default class extends Controller {
  connect() {
    let that = this;
    that.element.addEventListener('change', _.debounce(that.handleChange, 500))
  }
  handleChange(event) {
    event.preventDefault()
    // event.target.name // => "user[answer]"
    // event.target.value // => <user input string>
    event.target.form.requestSubmit()
  }
}

and here it's used in a form with a single text input. NOTE the controller is attached to the form, not to the inputs.

<%= turbo_frame_tag dom_id(form_model) do %>

      <%= form_with model: form_model,
        format: :turbo_stream,
        html: { data: { controller: "buttonless-form" } } do |f| %>

        <%= f.hidden_field :question_id, value: question.id %>

        <%= f.text_field :answer_value, class: "input shadow wide", placeholder: "Enter your answer here" %>
        
        
      <% end %> 

  <div id=<%= "question_#{question.id}_output" %>>
   <p> <!-- feedback to the user shows up here via Turbo -->
  </div>

<% end %> <!-- end turbo frame -->
aidan
  • 1,627
  • 17
  • 27