Blanks - Josh Brody Blanks | Josh Brody
All projects
Ruby Gem

Blanks

Form objects for Rails that actually feel like Rails.

Blanks is a form object library built on ActiveModel. You define attributes, validations, and associations the same way you do in ActiveRecord. It works with form_with, fields_for, and all the Rails form helpers without configuration. The tagline is “fill it in”—most forms don’t map to a single database table, and some don’t map to the database at all.

Why it exists

Form logic in Rails ends up in weird places. Controllers do too much. Models grow validation rules that only apply to certain contexts. You create a User but the form also needs a company name, terms acceptance, and a referral code—none of which belong on the User model.

The usual solution is a plain Ruby object that includes ActiveModel::Model. That works until you need nested attributes. Then you’re writing accepts_nested_attributes_for by hand, tracking IDs yourself, and wondering why Rails forms won’t cooperate.

Blanks handles all of that. Define your form, define your associations, and Rails just works.

How it works

Attributes are typed with optional defaults, same syntax as ActiveRecord. Associations use has_one and has_many that expect corresponding form classes. Nested attributes come for free—Blanks tracks IDs so it knows which records to update versus create.

class PostForm < Blanks::Base
  attribute :title, :string
  attribute :content, :string
  validates :title, presence: true

  has_many :images
end

class ImageForm < Blanks::Base
  attribute :url, :string
  attribute :caption, :string
end

That’s it. PostForm now accepts nested attributes for images, tracks which ones already exist in the database, and validates the whole tree.

Loading from models

Pull data from existing records without redefining everything:

class UserForm < Blanks::Base
  inherit_from User, except: [:created_at, :updated_at, :password_digest]
end

form = UserForm.from_model(user)
form.assign_attributes(params[:user])

model_attributes extracts just the top-level attributes for updating. attributes extracts everything including nested records—useful for accepts_nested_attributes_for on the actual model.

Dirty tracking

Know what changed since initialization:

form.title = "New title"
form.title_changed?  # true
form.title_was       # original value
form.changes         # { "title" => ["Old title", "New title"] }

Helpful for conditional logic in callbacks or skipping unchanged records.

Normalization

Strip whitespace, downcase emails, format phone numbers—whatever you need:

normalizes :email, with: ->(email) { email.strip.downcase }

The transformation runs on assignment. It’s idempotent, so assigning the same value twice won’t cause changed? to lie.

Stack

Ruby

Stay in the loop

Occasional essays on design, tools, and the craft of building things. No spam, unsubscribe anytime.

Ambient weather

The background of this site reflects the current weather and time of day in Saint Paul. The orbs shift in color and behavior based on what's happening outside my window.

Learn more about how this works