---
title: "Humane Rails Forms"
description: "Create forms that are easier for people to use"
---

# Humane Rails Forms

![A person shaking their first in disgust at the forms at their bank's website.](https://fly.io/ruby-dispatch/humane-rails-forms/assets/cover-cover.webp)

[Annie Ruygt](https://annieruygtillustration.com)

**How many times have you cursed at a date form field because it rejected `03/01/22` and wanted `03/01/2023`? Number inputs are the same—what if instead of copy & pasting from the calculator app you could enter `79 / 2` in a field and have it evaluate to `39.5`? Inputomatic makes this possible in your Rails app, which means less friction in your UIs for the people who use your app.**

It’s 2023 and here’s the best answer web browsers have given us to enter dates & times on popular browsers like Safari.

![Date picker in Safari](https://fly.io/ruby-dispatch/humane-rails-forms/assets/o907dZUtwtWdmffkJTwZOLYS.png)

And Chrome.

![Date picker in Chrome](https://fly.io/ruby-dispatch/humane-rails-forms/assets/rsa__ZTo6fcGg7Y8aD-GdXqy.png)

What’s maddening about them is they force people to conform to the computer. It should be the other way around—the computer should be more forgiving to the messiness of the human condition and conform to our imprecise inputs.

Let’s see if we can do better in one of the worlds most exciting and reviled class of applications: the expense reports!

## An Expense Reporting App

By far one of the most frustrating experiences of our lives is entering expense report data. Setting the date involves clicking on a bunch of different arrows, buttons, etc. to set the date on some poorly designed picker. Then, there’s always some policy that says “expense half of your cellphone bill”, we have to open a calculator to do simple calculations like `124.99 / 2`.

What if we could build forms that made sense of inputs like `Last week` for dates and `124.99 / 2` for numbers?

![Form that accepts inputs like "Last week" for a date and "124.99/2" for a number](https://fly.io/ruby-dispatch/humane-rails-forms/assets/rGXCbKYJoD0uLhyRbnuArb2i.png)

That’s a little better! When we save it we get what we’d expect: a date from last week and the calculated cost.

![Computed values are persisted to the database when forms are saved](https://fly.io/ruby-dispatch/humane-rails-forms/assets/YJQxdIDdvKdEe3ronCBxTpW6.png)

Let’s try creating an expense from 900 days ago, which is outside of the “90 days ago” policy in our expense account:

![Original values are preserved if there's a model validation error](https://fly.io/ruby-dispatch/humane-rails-forms/assets/APio78Gz8Qh7WE_tWc5-BvFF.png)

Rails preserves the input value until the validation is cleared, then it persists the actual date or number to the database.

[Try the demo app](https://inputomatic-demo.fly.dev/expenses/new) and enter a new expense. I promise it will be one of the most luxurious expenses you’ve ever entered. You can also [clone the demo app](https://github.com/rocketshipio/inputomatic-demo) and run it in your own machine or [deploy it to Fly.io](https://docs.fly.io/rails).

How does that work exactly?

## The ActiveModel `attribute` class method

When we look at the `expense.rb` [Active Record model code](https://github.com/rocketshipio/inputomatic-demo/blob/main/app/models/expense.rb), we see the class method `attribute` and pass it an `Inputomatic` class the casts the values to and from the model, form, and the database.

```
class Expense < ApplicationRecord
  attribute :purchased_at, Inputomatic::DateTime.new
  attribute :amount, Inputomatic::Number.new

  validates :purchased_at,
    inclusion: {
      in: Range.new(90.days.ago, 90.days.from_now),
      message: -> (object, data) { "#{data[:value].to_date} is not between #{90.days.ago.to_date} and #{90.days.from_now.to_date}" }
    }
end
```

[Inputomatic](https://github.com/rocketshipio/inputomatic) is a gem I created that defines a few casting behaviors between the value in the Active Record model and the database. Turns out you can do some pretty nifty things with `ActiveRecord::Type` classes that make forms more of a joy to use.

### The `ActiveRecord::Type::Value#cast_value` method

Let’s have a look at the `Inputomatic::Number` class, which is a subclass of `activerecord::Type::Value`:

```
class Number < ActiveRecord::Type::Value
  def cast_value(value)
    value.is_a?(String) ? ArithmeticInterpreter.new(value).parse : value
  end
end
```

The `cast_value` method is where all the magic happens. If the value is a `String`, it’s coming from a form as an arithmetic expression. [Inputomatic’s arithmetic interpreter](https://github.com/rocketshipio/inputomatic/blob/main/lib/inputomatic/arithmetic_interpreter.rb) (which was [written by my pair bear, GPT-4](https://fly.io/ruby-dispatch/pairing-with-gpt-4/)) parses the string `124.99/2` into an expression that it can safely evaluate (never *ever* run form input through `Kernel#eval`, which Stack Overflow would tell you to do) into a number.

Once we have a number, case closed! We have a float, integer, or decimal that can be persisted to the database.

If the value is *not* a `String`, then we just return the value assuming it will be handled by any upstream handlers. In this case we’ll assume its an `Integer` or `Float`.

## Fly.io ❤️ Rails

Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.

[Deploy a Rails app today! →](https://docs.fly.io/rails)

## “Pickers” and humanized form inputs can co-exist

If you still love date pickers, color pickers, etc. you can keep them! Humanized form inputs would read these values and do less interpretation to them before casting them into values the database can understand.

For people who want to skip the pickers, they’d get a better experience. For people who love clicking around on pickers, that would all still work too.

## Limitations

There’s always room for mistakes when a machine interprets something a human enters. It’s important to understand how important it is to capture precise information. For example, when we enter `last week` does that mean `7 days ago` or `The last day of last week`?

For an expense report application, there’s probably a policy that checks the dates to make sure it’s within the past 90 days. If the arithmetic of a float is off by $0.000000000001, its not the end of the world.

There are localization issues to think through too—for example in some countries one thousand is written as `1,000` and in others it’s written as `1.000`.

### Rails validations

Additionally, some Rails validations, like `validates :amount, numericality: { greater_than: 0 }` don’t work out of the box because they do regular expression tests against strings to check if they’re floats or integers.

```
# Rails validations check the string with regular expressions before the value is cast by `Inputomatic::Number.new`, which
# makes these validations not work properly.
validates :amount,
  numericality: { greater_than_or_equal_to: 0 }
```

If you tried to add a numericality validation to the demo, it would complain that `124.99/2` is not a number. This validation could probably be patched to perform validations after the value is cast.

### Chronice Time parsing gem

Inputomatic uses the [Chronic gem](https://github.com/mojombo/chronic) to parse dates, which has a lot of [issues](https://github.com/mojombo/chronic/issues) and [PRs](https://github.com/mojombo/chronic/pulls) that are worth understanding to get a feel for how well date parsing works.

If you enter the value `March 30` into a form on April 1, 2023, you’d be surprised to get back `March 30, 2024`. That’s because Chronic is currently only setup to handle partial dates in the future or the past—there’s no way to tell Chronic to “assume the current year if none is given”.

You could set Chronic’s context for the past, but then you’d be surprised to get the date `December 1, 2022` when you enter the value `December 1`.

## More ways to make your application forms more humane

This technique could be used in a lot of places including:

- **Telephone numbers** - Cast numbers like `800flowers` into numbers or infer the country code with a gem like [Phoney](https://github.com/floere/phony).
- **Countries or states** - If somebody enters `US`, `USA`, `United States of America`, etc. into a form field for country, the country code could be properly resolved when the value is cast. Same for states! The [Countries gem](https://github.com/countries/countries) could be a good start.
- **Colors** - Convert “black” to `#000`, “white” to `#fff`, etc.
- **URLs** - If you want to *always* ensure URLs have `https`, you could cast the String value into a `URL` object, set `URI#scheme = "https"`, then save out the string into the database so it’s always https. Maybe you just want to always strip off the trailing slash.
- **Capitalization** - If you have fields that must be stored in lowercase on the database, you could do it for the user automatically with a LowerCase type that always lower cases values.
- **Location** - I don’t recommend running strings through expensive API calls when casting a value, but if you can make the call “fast and cheap” enough, there’s nothing stopping you from converting a location like “Mexico City, Mexico” into a latitude longitude coordinate.

Whatever you choose, just make sure the wiggle room is acceptable for the application and the casting is fast enough.
