WIP Ruby form framework. While I'm building it out, please peruse the README below and let me know if its something you'd like to use. Ideas for improvement? Shoot!
- Flexibility: Decoupling models from user input allows for painfree changes to user interfaces and workflows.
- Reusability: Break forms down into discrete components to be composed in new and interesting ways.
- Implicit parameter sanitization: No need for whitelisting or strong_params when it's clear what fields exist on the form.
- Easy UI construction: Define template code once (or take advantage of semantic defaults) and easily reuse throughout your app
- Dynamic setting of options based on context: like showing certain states in a select box depending on user's country.
- Avoid double-nesting when using
fieldDSL: since it expands into a nestedembed, it'll double nest the namespace.
classUserForm < Forms::Component# Define on the class...field:namefield:phone_numberfield:accept_terms,:checkboxfield:gender,radio: {options: %i[malefemale]}field:state,select: {options: [["California","ca"],["Oregon","or"]]}field:pricing_plan,select: {options: :available_pricing_plans}privatedefsetup# ...or on the instancefield:pricing_plan,select: {options: available_pricing_plans}enddefavailable_pricing_plansifmy_model.is_cheap?["Free Plan","Plan 1","Plan 2"]else["Plan 3","Plan 4"]endendendclassCompanyForm < Forms::Componentfield:namefield:founded,MyCustomInputendclassEmploymentForm < Forms::Componentfield:date_hired,:datefield:titleembedUserFormembedCompanyFormembedEmailInputendclassAdminForm < Forms::Componentembed_manyCompanyFormendForms can render themselves to HTML:
form=MyForm.newform.renderNote that they do not include <form> or <input type="submit"> tags. Where
and how your form is submitted is up to you. In Rails, you might do something
like this:
<%= form_tag employment_path, method: :post do %><%= form.render %><%= submit_tag %><% end %>classUserForm < Forms::Componentembed:name,:text,validate: {presence: true}field:email,:text,validate: {presence: true,email: true}# Validations on this objectvalidate:name_is_uniquevalidate:user_can_sign_upprivatedefname_is_uniqueifname_is_not_unique?get(:name).errors << Forms::Error.new("Name must be unique")endenddefuser_can_sign_upunlesscool_enough_to_sign_up?errors << Forms::Error.new("Sorry you're not cool enough")endendendIf you need control over how and where your components are rendered but prefer not to implement custom components with accompanying templates, you can render them individually:
<div><%=form.get(:name).render%><p>Lorem ipsum...</p></div><ul><li><%=form.get(:date_field).get(:day).render%></li></ul>Forms are composed of objects that inherit from Component and implement
the Component API. As such they possess the ability to be nested in all sorts
of fun ways. Understanding this structure is essential to customizing your
implementations.
All components implement the following API:
initialize(namespace, options)where namespace is an array in increasing order of specificityvalue=(value)valueparse(params)render
Component has default implementations for all of these, see
lib/forms/component.rb
Components nest other components and are responsible for rendering their children as well as setting/getting them. Setting values and parsing params are passed down the tree to the relevant components. Retrieving data reaches down the tree to pull cleansed input back up.
Now that we understand the basics of the framework, let's build a custom set of components to collect date input in three separate fields: one for day, month, and year.
With nested inputs:
classDateInput < Forms::Componentembed:day,:textembed:month,:textembed:year,:textdefvalue=(date)get(:day).value=date.dayget(:month).value=date.monthget(:year).value=date.yearenddefvalueDate.newget(:year).value,get(:month).value,get(:day).valueend# The default implementations of `initialize`, `parse` and `render` will do# just fine for this input.endAll on one component:
classDateInput < Forms::Component# The default implementations of `initialize` will do just finedefvalue=(date)@day=date.day@month=date.month@year=date.yearenddefvalueDate.new(@year,@month,@day)enddefparse(params)@year=params[:year]@month=params[:month]@day=params[:day]enddefrender# render some HTML with 3 inputsendendclassMyForm < Forms::Componentembed:birthday,DateInputendform = MyForm.new
form.get(:birthday).renderPairing a label with an input is a common use case. Forms ships with a default
Field component for exactly this purpose.
classUserForm < Forms::Component# Use a Boolean type fieldfield:is_admin,:checkbox# Essentially a shorthand for:embed:is_admin_field,Forms::Fielddoembed:is_admin,Forms::CheckboxInputendendForms comes with a grip of standard inputs:
Forms::Text, shorthand::textForms::Textarea, shorthand::textareaForms::Checkbox, shorthand::checkboxForms::Radio, shorthand::radio(future)Forms::Select, shorthand::select(future)
This part is up to you. You might prefer to implement load and save methods
on their form objects, or maybe you pass your form object and models to a
service object to handle that. You could even do it in your controller. Forms
has no opinion on this.
classEmploymentsController < ApplicationControllerdefnew@user_form=UserForm.newenddefcreate@user_form=UserForm.new@user_form.parse(params)if@user_form.save# ...elserender:newendenddefedit@user_form=UserForm.new(User.find(params[:id]))enddefupdate@user_form=UserForm.new(User.find(params[:id]))@user_form.parse(params)if@user_form.save# ...elserender:newendendendForm objects render the contents of a form without a wrapping <form> tag or
submit button. Because where and how you submit your form can be unique to
different use cases, we leave that up to you.
<h1>Become an Employee!</h1><%= form_tag employment_path, method: :post do %><%= @employment_form.render %><%= submit_tag %><% end %>