Escaping the traditional Rails form
Tags: webdev, rails, programming, html
Published: 2022-03-10 17:30:49

Theres been a pattern I've seen creeping in Rails apps. The pattern is that there are cases where someone needs to "escape" a form and provide a different action.

For example, lets save I have a form with both a `delete` button and a `save` button next to each other for something like a comment. Here's what that may look like:

```html
<form action="/comments" method="post">
  <label for="content">Content</label><br>
  <textarea id="content" name="comment[content]"></textarea>
  <br>
  <button>Save</button>
  <button>Delete</button>
</form>
```

Or if you're using Rails, you may do something like this:

```erb
<%= form_with model: Comment.new do |form| %>
  <%= form.label :content %>
  <br>
  <%= form.textarea :content %>
 
  <br>

  <%= form.submit "Save" %>
  <%= form.submit "Delete" %>
</form>
```

Now you might be thinking these 2 are equal. But they're not. Under the hood `form.submit` creates an `<input>` tag and sets the `value` to `"save"` so it looks like this:

```html
<!-- Generated by form.submit -->
<input type="submit" value="Save">
<input type="submit" value="Delete">
```

Now this is _ok_, but the problem is `<input>` doesn't accept nested HTML tags. So you would need a button if you wanted to show an icon or something similar. 

The other problem we're presented with is if we want to go to a different route or use a different method, we may be tempted to use `button_to`, but this generates a full form wrapper and forms cannot be nested inside of other forms.

So let's see how we could generate a button instead to get around these limitations. In addition, submit buttons are far more flexible than submit inputs.

To generate a submit button from a Rails form, instead of `form.submit` you can use `form.button` which of course I don't see anywhere in the Rails documentation for form helpers, but the tag helper is here:

https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-button_tag

Moving on, lets see what it looks like at full speed:

```erb
<%= form_with model: @comment do |form| %>
  <%= form.label :content %>
  <br>
  <%= form.textarea :content %>
 
  <br>

  <%= form.button "Save", type: :submit %>
  <%= form.button "Delete", type: :submit %>
</form>
```

And that's it! You now have a submit button. By default, Rails generates a `<button type="default">` which technically isn't an allowable type, so to be safe, I just pass in the real type. (Perhaps a PR for this should be made, but thats for another day.)

`f.button` has the added benefit of accepting a block unlike a `f.submit` so you could do something like this to show a "save icon" next to the save text.

```erb
<%= f.button type: :submit do %>
  <i class="fa-thin fa-floppy-disk"></i> 
  Save
<% end %>
```

Good luck doing that with an `<input>`!

### Escaping the form

Now if you've been following along this far, you may have noticed that theres no way to disambiguate the two buttons if they're both going to the same route.

An easy way around this is to provide a `name` and `value` attribute to your buttons so they get submitted with the form and you can then on your backend do a `params[]` check to see what was submitted.

```erb
<%= form.button "Save", type: :submit, name: "commit_type", value: "save" %>

<%= form.button "Delete", type: :submit, name: "commit_type", value: "destroy" %>
```

Now, in your Rails controller you could do:

```rb
def update
  if params[:commit_type] == "save"
    # save it!
  elsif params[:commit_type] == "destroy"
    # get rid of it!
  end
end
```

Now this is _okay_ but may not be the best way to handle a `destroy` action since you should have a dedicated route for that. Instead, we can change the `formaction` on the button to point to go to our delete path.

```erb
<%= form.button "Delete", type: :submit, formmethod: :delete %> 
```

This will now override the `method` set on the form and make it a `DELETE` request!

Now let's take it one step further, maybe you need to send the delete request to a different path. You can do so by passing in a `formaction` to the button like so:

```erb
<%= form.button "Delete", type: :submit, formmethod: :delete, action: "/super-secret-url" %>
```

Alright, maybe you don't have `/super-secret-url` but you get the point.

The final thing we can do with a button is submit with an arbitrary form!

For example, let's say I have a `Logout` button in my header than needs to submit a `delete` request to the backend.

Most Rails devs are familiar with the old Rails-UJS way of using a link, but this isn't really a link like this:

```erb
<%= link_to "Log out", "/logout", data: { method: "delete" } %>
```

This _works_ but you now have a link functioning as a button inside of a form and it is not as clear to screen readers what this link actually does since links are technically only supposed to be `GET` requests.

What if instead of this, you could have a hidden form on the page and tell the button to submit using that form! It's 100% possible!

```erb
<!-- Forms only technically support GET / POST. Rails does some magic to make it work properly. -->
<form id="logout-form" action="/logout" method="post" class="hidden"><form>

<!-- other stuff -->

<nav>
  <!-- Its worth noting, formmethod only supports GET / POST, but when submitted with Turbo works some magic to send a DELETE. -->
  <button formmethod="delete" form="logout-form"> Logout </button>
</nav>
```

That's right! You can mix and match `formmethod`, `formaction`, and `form`!

`form` expects the `id` of the form you would like the button to submit with! This means you can have a button submit to a form from anywhere in your page without having it inside of the form! Pretty nifty! Buttons are pretty cool!

To read more about buttons, MDN outlines all the cool properties and attributes. 

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes

### Additional notes

Technically the `formmethod` of a button only supports `post` and `get`. We use `delete` in this post because Turbo is able to infer the proper `formmethod` for us and send the appropriate fetch request.

I have not tested if this technique works with Turbolinks or Rails-UJS. I do know it is supported by Mrujs and Turbo.

Anyways, hope this was helpful and stop nesting links in forms especially if you're using Turbo!

If you are limited by the formmethod issue, it is recommended to construct a `<form>` using the Rails helpers with the proper `method` like this:

```erb
<%= form_with id: "delete-model", model: @model, method: :delete, class: "hidden" %>

<%= form_with model: @model, method: :put do |form| %>
  <%= form.button "Delete", type: :submit, form: "delete-model" %>
<% end %>
```

The `form` attribute will override the form that the button is currently nested in and avoids the limitations of the `formmethod`. Anyways, happy hunting and I hope this was a useful guide to all the fun ways to use buttons with forms and how to escape the generic nesting a button in a form and unlocks some new UI / UX potential for you!