Programmatic Form Submissions with Turbo Streams

29 Sep 2021

Recently I was carrying out a piece of Rails work that required the triggering of a form submission programmatically from a Stimulus controller. The form essentially posted some markdown to the server that needed to be converted to HTML and then inserted back into the page to preview via the turbo_stream.update tag. Below is code for the form I was trying to submit:

<%= form_with url: admin_preview_index_path, data: { preview_target: "previewForm", action: 'turbo:submit-end->preview#previewComplete' }, class: 'hidden' do |form| %>
  <%= form.text_area :markdown, value: @post.body, data: { preview_target: 'previewFormMarkdown' } %>
  <%= form.submit '', data: { preview_target: 'previewFormButton' } %>
<% end %>

Creating the HTML:

<form class="hidden" data-preview-target="previewForm" data-action="turbo:submit-end->preview#previewComplete" action="/admin/preview" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="Ou_-JLUFmK-SAetB2MSIKCj1Sw9ErrmZFkc9ltMGQJts1UlKzwPO9F3tCCvh81L0KyUMxzgUo_KMq8mVpc9I5Q">
  <textarea data-preview-target="previewFormMarkdown" name="markdown" id="markdown"></textarea>
  <input type="submit" name="commit" value="" data-preview-target="previewFormButton" data-disable-with="">
</form>

Using submit()

My initial solution was to grab the <form> element and use javaScript to trigger the submit() function the form has:

this.previewFormTarget.submit()

This worked for form submission as you would expect, but it prevented Turbo from hooking into the turbo stream response and adding it to the DOM. Instead I would just get the raw HTML with no styles rendered to the page. The location of the window would change.

Using click()

The first option for getting around this is issue to pretend to be a user of the application and programmatically click the submit button of the form:

this.previewFormButtonTarget.click()

This submits the form and Turbo now knows how to handle the response appropriately (append/replace/prepend etc) and manipulates the DOM with the returned HTML rather than changing the window location.

Using requestSubmit()

The second option is a function called form.requestSubmit(). It allows you to programmatically submit a form using a specific submit button for the form, but if not passed an argument will use the first submit button available:

this.previewForm.requestSubmit()

This will now cause the form to be submitted in the same way as for click().

Note: If you're supporting Safari (which you probably are) you'll need to pull in a polyfill for requestSubmit().

Conclusion

Turbo is configured to intercept the form button click() event rather than the form submit() event. Therefore, if you need to programmatically submit a form in the turbo stream format and have Turbo handle the response appropriately you can act like an application user and use click() on the submit button of the form or use the requestSubmit() function instead.

References