Formed

Formed is the form object pattern you never knew you needed: uses ActiveModel under the hood, and supports associations just like ActiveRecord.

Contents

Usage

Formed form objects act just like the ActiveRecord models you started forcing into form helpers.

Basic form

class ProductForm < Formed::Base 
  acts_like_model :product 

  attribute :title 
  attribute :content, :text 
end

With validations

Use all the validations your heart desires.

class PostForm < Formed::Base 
  acts_like_model :post 

  attribute :title  
  attribute :content, :text 

  validates :title, presence: true 
  validates :content, presence: true 
end

Associations

Here's the big one:

class TicketForm < Formed::Base 
  acts_like_model :ticket 

  attribute :name

  # automatically applies accepts_nested_attributes_for
  has_many :ticket_prices, class_name: "TicketPriceForm"

  validates :name, presence: true  
end
class TicketPriceForm < Formed::Base
  acts_like_model :ticket_price

  attribute :price_in_cents, :integer

  validates :price_in_cents, presence: true, numericality: { greater_than: 0 }
end

Context

Add context:

class OrganizationForm < Formed::Base 
  acts_like_model :organization 

  attribute :location_id, :integer 

  def location_id_options
    context.organization.locations
  end
end
form = OrganizationForm.new
form.with_context(organization: @organization)

Context gets passed down to all associations too.

class OrganizationForm < Formed::Base 
  acts_like_model :organization 

  has_many :users, class_name: "UserForm"

  attribute :location_id, :integer 

  def location_id_options
    context.organization.locations
  end
end
form = OrganizationForm.new 
form.with_context(organization: @organization)
user = form.users.new
user.context == form.context # true 

Suggestions

Let forms be forms, not forms with actions

Forms should only know do one thing: represent a form and the form's state. Leave logic to its own.

If you use something like ActiveDuty, you could do this:

class MyCommand < ApplicationCommand
  def initialize(form)
    @form = form 
  end

  def call
    return broadcast(:invalid, form) unless @form.valid? 

    # ...
  end
end

Contributing

By submitting a Pull Request, you disavow any rights or claims to any changes submitted to the Formed project and assign the copyright of those changes to joshmn.

If you cannot or do not want to reassign those rights (your employment contract for your employer may not allow this), you should not submit a PR. Open an issue and someone else can do the work.

This is a legal way of saying "If you submit a PR to us, that code becomes ours". 99.99% of the time that's what you intend anyways; we hope it doesn't scare you away from contributing.

Acknowledgements

This was heavily inspired by — and tries to be backwards compatible with — AndyPike's Rectify form pattern.