How to Work with Forms inside Forms in Rails

Abstract, 3D-like image in a duotone color scheme depicting the concept of nesting with interlocking geometric shapes or smooth curves, evoking a sense of order and harmony

Have a look at the following screenshot from one of my SaaS’.

User interface of an email scheduling application with dropdown filters for Name, Email, and Status set to 'Scheduled' and a Save button.

It’s a form to edit a “filter”, and inside is a button to “Delete…” said filter. Have a think on it: how would you do this in your typical Hotwired-Rails app?

Maybe your first idea is to write this:

link_to "Delete…", filter_path(filter), data: {turbo_method: :delete}

And that will work, but it’s not the right way semantically and for accessibility. link_to is for navigation and retrieving data without side effects, whereas button_to is used for actions like deleting a resource, as it generates a form with built-in security features, for both safety and semantic reasons. Also what happens if the user opens the “page” in a new tab?

Alright, easy! Rewrite it to this:

button_to "Delete…", filter_path(filter), method: :delete, data: {turbo_method: :delete}

This is the output of above line:

<form class="button_to" method="post" action="/filters/1234">
  <input type="hidden" name="_method" value="delete" autocomplete="off">
  <button data-turbo-method="delete" type="submit">Delete…</button>
  <input type="hidden" name="authenticity_token" value="LONG_STRING" autocomplete="off">
</form>

Now you have a form inside a form! Besides being invalid HTML, you will notice it won’t even work! Sigh!

The solution to nested forms

The solution is to use the HTML5 attribute: form. First create another form (above, below, or wherever on the same page as the other form).

form_with model: filter, method: :delete, data: {turbo_method: "delete"}, id: "destroy_filter_form"

Take note of the id (destroy_filter_form).

Then inside the other form, add a button like so:

form_with model: filter do |form|
# …
  button_tag "Delete…", type: :submit, form: "destroy_filter_form"
# …
end

The important bit is the form attribute with the value of the form it should trigger (destroy_filter_form).

The PRO solution to nested forms

And now for the final act! It’s even possible to write it all in one line:

button_tag "Delete…", type: :submit, formmethod: :post, formaction: filters_path(filter), data: {turbo_method: "delete"} %>

Key bits are the formaction which overrides the parent form’s action attribute. formmethod is used to make sure it’s a :post request (the parent could be :put or :get).

The last solution is the one I typically use for straightforward actions like “delete”. If there are more (hidden) attributes needed, I go for the other form solution.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months