Skip to content

Repository files navigation

Forms

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!

Key Concepts & Goals

  1. Flexibility: Decoupling models from user input allows for painfree changes to user interfaces and workflows.
  2. Reusability: Break forms down into discrete components to be composed in new and interesting ways.
  3. Implicit parameter sanitization: No need for whitelisting or strong_params when it's clear what fields exist on the form.
  4. Easy UI construction: Define template code once (or take advantage of semantic defaults) and easily reuse throughout your app

Need to address

  1. Dynamic setting of options based on context: like showing certain states in a select box depending on user's country.
  2. Avoid double-nesting when using field DSL: since it expands into a nested embed, it'll double nest the namespace.

Form Objects

Simple Forms

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,MyCustomInputend

Nested Forms (future)

classEmploymentForm < Forms::Componentfield:date_hired,:datefield:titleembedUserFormembedCompanyFormembedEmailInputend

Nested Collections (future)

classAdminForm < Forms::Componentembed_manyCompanyFormend

Rendering

Forms can render themselves to HTML:

form=MyForm.newform.render

Note 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 %>

Validations (future)

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")endendend

Granular Rendering (future)

If 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>

Customizing

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.

Component API

All components implement the following API:

  • initialize(namespace, options) where namespace is an array in increasing order of specificity
  • value=(value)
  • value
  • parse(params)
  • render

Component has default implementations for all of these, see lib/forms/component.rb

Nesting

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.

Building Custom Components

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.end

All 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 inputsendend
classMyForm < Forms::Componentembed:birthday,DateInputend
form = MyForm.new
form.get(:birthday).render

Built-in Conveniences

Fields

Pairing 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::CheckboxInputendend

Inputs

Forms comes with a grip of standard inputs:

  • Forms::Text, shorthand: :text
  • Forms::Textarea, shorthand: :textarea
  • Forms::Checkbox, shorthand: :checkbox
  • Forms::Radio, shorthand: :radio (future)
  • Forms::Select, shorthand: :select (future)

Loading & Saving

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.

Example Rails Usage

Controller (future)

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:newendendend

View

Form 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 %>

About

Ruby form framework

Resources

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages