ActionForm
This library allows you to build complex forms in Ruby with a simple DSL. It provides:
- A clean, declarative syntax for defining form fields and validations
- Support for nested forms
- Automatic form rendering with customizable HTML/CSS
- Built-in error handling and validation
- Integration with Rails and other Ruby web frameworks
Installation
Add this line to your application's Gemfile:
gem 'action_form'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install action_form
Requirements
- Ruby >= 2.7.0
- Rails >= 6.0.0 (for Rails integration)
Concept
ActionForm is built around a modular architecture that separates form definition, data handling, and rendering concerns.
Core Architecture
ActionForm::Base is the main form class that inherits from Phlex::HTML
for rendering. It combines three key modules:
-
Elements DSL: Provides methods like
element
,subform
, andmany
to define form structure using block syntax, where each element can be configured with input types, labels, and validation options - Rendering: Converts form elements into HTML using Phlex, handles nested forms, error display, and provides JavaScript for dynamic form interactions
- Schema DSL: Defines how form data is structured and validated using EasyParams. It generates parameter classes that can process submitted form data and restore the form state when validation fails
How It Works
-
Form Definition: You define your form using a declarative DSL with
element
,subform
, andmany
methods -
Element Creation: Each element definition creates a class that inherits from
ActionForm::Element
. The element name must correspond to a method or attribute on the object passed to the form (e.g.,element :name
expects the object to have aname
method) - Instance Building: When the form is instantiated, it iterates through each defined element and creates an instance. Each element instance is bound to the object and can access its current values, errors, and HTML attributes
- Rendering: The form renders itself using Phlex, with each element containing all the data needed to render a complete form control (input type, current value, label text, HTML attributes, validation errors, and select options)
-
Parameter Handling: The form automatically generates EasyParams classes that mirror the form structure, providing type coercion, validation, and strong parameter handling for form submissions. Each element's
output
configuration determines how submitted data is processed
Key Features
- Declarative DSL: Define forms with simple, readable syntax
-
Nested Forms: Support for complex nested structures with
subform
andmany
- Dynamic Collections: JavaScript-powered add/remove functionality for many relationships
- Flexible Rendering: Each element can be configured with custom input types, labels, and HTML attributes
- Error Integration: Built-in support for displaying validation errors
- Rails Integration: Seamless integration with Rails forms and parameter handling
Data Flow
ActionForm follows a bidirectional data flow pattern that handles both form display and form submission:
Phase 1: Form Display
- Object/Model: Your Ruby object (User model, ActiveRecord instance, or plain Ruby object) containing data to display
-
Form Definition: ActionForm class defined using the DSL (
element
,subform
,many
methods) - Element Instances: Each form element becomes an instance bound to the object, with access to current values, errors, and HTML attributes
- HTML Rendering: Final HTML output rendered using Phlex, ready for the browser
Phase 2: Form Submission
- User Input: Data submitted through the form by the user
- Parameter Validation: ActionForm's auto-generated EasyParams classes validate and coerce submitted data
- Form Processing: Your application logic processes the validated data (database saves, business logic, etc.)
- Response: Result sent back to user (success page, error display, redirect, etc.)
Key Benefits:
- Single Source of Truth: The same form definition handles both displaying existing data and processing new data
- Automatic Parameter Handling: EasyParams classes are automatically generated to mirror your form structure
- Error Integration: Failed validations can re-render the form with submitted data and error messages
-
Nested Support: Both phases support complex nested structures through
subform
andmany
relationships
Usage
ActionForm follows a Declare/Plan/Execute pattern that separates form definition from data handling and rendering:
-
Declare: Define your form structure using the DSL (
element
,subform
,many
) - Plan: ActionForm creates element instances bound to your object's actual values
- Execute: Each element renders itself with the appropriate HTML, labels, and validation
Form elements declaration
ActionForm provides a declarative DSL for defining form elements. Each form class inherits from ActionForm::Base
and uses three main methods to define form structure:
Basic Elements
Use element
to define individual form fields:
class UserForm < ActionForm::Base
element :name do
input type: :text, class: "form-control"
output type: :string, presence: true
label text: "Full Name", class: "form-label"
end
element :email do
input type: :email, placeholder: "user@example.com"
output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
end
element :age do
input type: :number, min: 0, max: 120
output type: :integer, presence: true
end
end
Available Input Types
-
HTML input types:
:text
,:email
,:password
,:number
,:tel
,:url
, etc. -
Selection inputs:
:select
-
Other inputs:
:textarea
,:hidden
Available Output Types
ActionForm uses EasyParams for parameter validation and type coercion:
-
Basic types:
:string
,:integer
,:float
,:bool
,:date
,:datetime
-
Collections:
:array
(withof:
option for element type) -
Validation options:
presence: true
,format: regex
,inclusion: { in: [...] }
Element Configuration Methods
element :field_name do
# Input configuration
input type: :text, class: "form-control", placeholder: "Enter value"
# Output/validation configuration
output type: :string, presence: true, format: /\A\d+\z/
# Label configuration
label text: "Custom Label", class: "form-label"
# Select options (for select, radio, checkbox)
options [["value1", "Label 1"], ["value2", "Label 2"]]
# Elements tagging
tags column: "1"
end
Nested Forms
Use subform
for single nested objects:
class UserForm < ActionForm::Base
subform :profile do
element :bio do
input type: :textarea, rows: 4
output type: :string
end
element :avatar do
input type: :file
output type: :string
end
end
end
Use many
for collections of nested objects. Note that many
requires a subform
block inside it:
class UserForm < ActionForm::Base
many :addresses do
subform do
element :street do
input type: :text
output type: :string, presence: true
end
element :city do
input type: :text
output type: :string, presence: true
end
end
end
end
Complete Example
class UserForm < ActionForm::Base
element :name do
input type: :text, class: "form-control"
output type: :string, presence: true
label text: "Full Name"
end
element :email do
input type: :email, class: "form-control"
output type: :string, presence: true
end
element :role do
input type: :select, class: "form-control"
output type: :string, presence: true
options [["admin", "Administrator"], ["user", "User"]]
end
element :interests do
input type: :checkbox
output type: :array, of: :string
options [["tech", "Technology"], ["sports", "Sports"], ["music", "Music"]]
end
subform :profile do
element :bio do
input type: :textarea, rows: 4
output type: :string
end
end
many :addresses do
subform do
element :street do
input type: :text
output type: :string, presence: true
end
element :city do
input type: :text
output type: :string, presence: true
end
end
end
end
Tagging system
ActionForm includes a flexible tagging system that allows you to add custom metadata to form elements and control rendering behavior. Tags serve multiple purposes:
Purpose of Tags
- Rendering Control: Tags control how elements are rendered (e.g., showing error messages, template rendering)
- Custom Metadata: Store custom data that can be accessed during rendering
- Element Classification: Mark elements with specific characteristics for conditional logic
Automatic Tags
ActionForm automatically adds several tags based on element configuration:
element :email do
input type: :email
output type: :string
options [["admin", "Admin"], ["user", "User"]] # Adds options: true tag
end
# Automatic tags added:
# - input: :email (from input type)
# - output: :string (from output type)
# - options: true (from options method)
# - errors: true/false (based on validation errors)
Custom Tags
Add custom tags using the tags
method:
element :password do
input type: :password
output type: :string, presence: true
# Custom tags
tags row: "3",
column: "4",
background: "gray"
end
Tag Usage in Rendering
Tags are used throughout the rendering process:
# Error display (automatic)
render_inline_errors(element) if element.tags[:errors]
# Custom rendering logic
def render_element(element)
if element.tags[:row] == "3"
div(class: "high-priority") { super }
else
super
end
end
Nested Form Tags
Tags are automatically propagated in nested forms:
many :addresses do
subform do
element :street do
input type: :text
tags required: true
end
end
end
# Each address element will have:
# - input: :text
# - subform: :addresses (added automatically)
# - required: true (from custom tag)
Practical Examples
Conditional Styling:
element :email do
input type: :email
tags field_type: "contact"
end
# In your form class:
def render_input(element)
super(class: css_class)
span do
help_info[element.tags[:field_type]]
end
end
Custom Error Handling:
element :username do
input type: :text
tags custom_validation: true
end
# Override error rendering:
def render_inline_errors(element)
if element.tags[:custom_validation]
div(class: "custom-errors") { element.errors_messages.join(" | ") }
else
super
end
end
The tagging system provides a powerful way to extend ActionForm's behavior without modifying the core library, enabling custom rendering logic and element classification.
Rendering process
ActionForm uses a hierarchical rendering system built on Phlex that allows complete customization of HTML output. The rendering process follows a clear flow from form-level down to individual elements.
Rendering Flow
view_template (main entry point)
↓
render_form (form wrapper)
↓
render_elements (iterate through all elements)
↓
render_element (individual element)
↓
render_label + render_input + render_inline_errors
Core Rendering Methods
Form Level:
-
view_template
- Main entry point, defines overall form structure -
render_form
- Renders the<form>
wrapper with attributes -
render_elements
- Iterates through all form elements -
render_submit
- Renders the submit button
Element Level:
-
render_element
- Renders a complete form element (label + input + errors) -
render_label
- Renders the element's label -
render_input
- Renders the input field -
render_inline_errors
- Renders validation error messages
Subform Level:
-
render_subform
- Renders a single nested form -
render_many_subforms
- Renders collections of nested forms with JavaScript -
render_subform_template
- Renders templates for dynamic form addition
Customizing Rendering
You can override any rendering method in your form class to customize the HTML output:
Basic Customization:
class UserForm < ActionForm::Base
element :name do
input type: :text
output type: :string, presence: true
end
# Override element rendering to add custom wrapper
def render_element(element)
div(class: "form-group") do
super
end
end
# Customize label rendering
def render_label(element)
div(class: "label-wrapper") do
super
end
end
# Customize input rendering
def render_input(element, **html_attributes)
div(class: "input-wrapper") do
super(class: "form-control")
end
end
end
Bootstrap-Style Layout:
class UserForm < ActionForm::Base
element :name do
input type: :text
output type: :string, presence: true
end
# Bootstrap grid layout
def render_element(element)
div(class: "row mb-3") do
render_label(element)
render_input(element)
render_inline_errors(element) if element.tags[:errors]
end
end
def render_label(element)
div(class: "col-md-3") do
super(class: "form-label")
end
end
def render_input(element, **html_attributes)
div(class: "col-md-9") do
super(class: "form-control")
end
end
end
Conditional Rendering:
class UserForm < ActionForm::Base
element :email do
input type: :email
tags field_type: "contact"
end
element :password do
input type: :password
tags field_type: "security"
end
# Conditional rendering based on tags
def render_element(element)
case element.tags[:field_type]
when "contact"
div(class: "contact-field") { super }
when "security"
div(class: "security-field") { super }
else
super
end
end
end
Custom Error Rendering:
class UserForm < ActionForm::Base
element :username do
input type: :text
output type: :string, presence: true
end
# Custom error display
def render_inline_errors(element)
if element.tags[:errors]
div(class: "alert alert-danger") do
strong { "Error: " }
element.errors_messages.join(", ")
end
end
end
end
Custom Submit Button:
class UserForm < ActionForm::Base
# Custom submit button with styling
def render_submit(**html_attributes)
div(class: "form-actions") do
super(class: "btn btn-primary", **html_attributes)
end
end
end
Complete Form Layout Override:
class UserForm < ActionForm::Base
element :name do
input type: :text
output type: :string, presence: true
end
# Override the entire form structure
def view_template
div(class: "custom-form") do
h2 { "User Registration" }
render_elements
div(class: "form-footer") do
render_submit
a(href: "/cancel") { "Cancel" }
end
end
end
end
Advanced Customization
Custom Input Types:
class UserForm < ActionForm::Base
element :rating do
input type: :text
tags custom_input: "rating"
end
# Custom input rendering for specific types
def render_input(element, **html_attributes)
if element.tags[:custom_input] == "rating"
render_rating_input(element, **html_attributes)
else
super
end
end
private
def render_rating_input(element, **html_attributes)
div(class: "rating-input") do
5.times do |i|
input(type: "radio",
name: element.html_name,
value: i + 1,
checked: element.value == i + 1)
end
end
end
end
Dynamic Form Structure:
class UserForm < ActionForm::Base
element :name do
input type: :text
tags section: "basic"
end
element :email do
input type: :email
tags section: "contact"
end
# Group elements by sections
def render_elements
sections = elements_instances.group_by { |el| el.tags[:section] }
sections.each do |section_name, elements|
div(class: "form-section", id: section_name) do
h3 { section_name.to_s.capitalize }
elements.each { |element| render_element(element) }
end
end
end
end
The rendering system provides complete flexibility while maintaining the declarative nature of form definition. You can customize as little or as much as needed, from individual elements to the entire form structure.
Element
The ActionForm::Element
class represents individual form elements and provides methods to access their data, control rendering, and customize behavior. Each element is bound to an object and can access its current values, errors, and HTML attributes.
Core Methods
value
- Gets the current value from the bound object
element :name do
input type: :text
def value
# Default: object.name
object.other_name
end
end
html_value
- Formats value for HTML (dafault value.to_s
):
element :name do
input type: :text
def html_value
value.strftime('%Y-%m-%d') # Format before render
end
end
render?
- Controls whether the element should be rendered:
element :admin_field do
input type: :text
def render?
object.admin?
end
end
# Or conditionally render elements:
def render_elements
elements_instances.select(&:render?).each do |element|
render_element(element)
end
end
detached?
- Indicates if the element is detached from the object (uses static values):
element :static_field do
input type: :text, value: "Static Value"
def detached?
true # This element doesn't bind to object values
end
end
Label Methods
label_text
- Gets the text to display in the label:
element :full_name do
input type: :text
label text: "Complete Name", class: 'cool-label', id: 'full-name-label-id'
end
display: false
- Label won't be rendered
element :full_name do
input type: :text
label display: false
end
Element Properties
name
- The element's name (symbol):
element :username do
input type: :text
end
# Access the name:
element.name # => :username
tags
- Access to element tags:
element :priority_field do
input type: :text
tags priority: "high", section: "important"
end
element.tags[:priority] # => "high"
element.tags[:section] # => "important"
errors_messages
- Validation error messages:
element :email do
input type: :email
output type: :string, presence: true
end
# When validation fails:
element.errors_messages # => ["can't be blank", "is invalid"]
disabled?
- Controls whether the element is disabled:
element :username do
input type: :text
def disabled?
object.persisted? # Disable for existing records
end
end
readonly?
- Controls whether the element is readonly:
element :email do
input type: :email
def readonly?
object.verified? # Readonly if email is verified
end
end
Element Lifecycle
Elements go through several phases:
- Definition - Element class is created with DSL configuration
- Instantiation - Element instance is created and bound to object
-
Rendering - Element is rendered to HTML (if
render?
returns true) - Validation - Element values are validated during form submission
class UserForm < ActionForm::Base
element :name do
input type: :text
output type: :string, presence: true
end
# Customize any phase:
def render_element(element)
if element.render?
div(class: "form-group") do
render_label(element)
render_input(element)
render_inline_errors(element) if element.tags[:errors]
end
end
end
end
Rails integration
ActionForm provides seamless integration with Rails through ActionForm::Rails::Base
, which extends the core functionality with Rails-specific features like automatic model binding, nested attributes, and Rails form helpers.
Rails Form Class
Use ActionForm::Rails::Base
instead of ActionForm::Base
for Rails applications:
class UserForm < ActionForm::Rails::Base
resource_model User
element :name do
input type: :text
output type: :string, presence: true
end
element :email do
input type: :email
output type: :string, presence: true
end
end
Model Binding
The resource_model
method automatically configures the form for your Rails model:
class UserForm < ActionForm::Rails::Base
resource_model User # Sets up automatic parameter scoping and model binding
end
# In your controller:
def new
@form = UserForm.new(model: User.new)
end
def create
@form = UserForm.new(model: User.new, params: params)
if @form.class.params_definition.new(params).valid?
# Process the form
else
render :new
end
end
Parameter Scoping
ActionForm automatically handles Rails parameter scoping:
class UserForm < ActionForm::Rails::Base
resource_model User # Automatically scopes to 'user' parameters
end
# Form parameters are automatically scoped to:
# params[:user][:name]
# params[:user][:email]
# etc.
You can also set custom scopes:
class AdminUserForm < ActionForm::Rails::Base
scope :admin_user # Parameters will be scoped to params[:admin_user]
end
Nested Attributes for many Relations
Rails integration automatically handles nested attributes for many
relationships:
class UserForm < ActionForm::Rails::Base
resource_model User
element :name do
input type: :text
output type: :string, presence: true
end
many :addresses do
subform do
element :street do
input type: :text
output type: :string, presence: true
end
element :city do
input type: :text
output type: :string, presence: true
end
end
end
end
Automatic Features:
- Primary key elements (
id
) are automatically added for existing records - Delete elements (
_destroy
) are automatically added for removal - Parameters are properly scoped with
_attributes
suffix - JavaScript for dynamic add/remove functionality
Generated Parameters:
# For addresses, parameters look like:
params[:user][:addresses_attributes] = {
"0" => { "id" => "1", "street" => "123 Main St", "city" => "Anytown" },
"1" => { "id" => "2", "street" => "456 Oak Ave", "city" => "Somewhere", "_destroy" => "1" }
}
Controller Integration
class UsersController < ApplicationController
def new
@form = UserForm.new(model: User.new)
end
def create
@form = UserForm.new(model: User.new, params: params)
user_params = @form.class.params_definition.new(params)
if user_params.valid?
@user = User.create!(user_params.user.to_h)
redirect_to @user
else
@form = @form.with_params(user_params)
render :new
end
end
def edit
@form = UserForm.new(model: @user)
end
def update
@form = UserForm.new(model: @user, params: params)
user_params = @form.class.params_definition.new(params)
if user_params.valid?
@user.update!(user_params.user.to_h)
redirect_to @user
else
@form = @form.with_params(user_params)
render :edit
end
end
end
View Integration
<!-- app/views/users/new.html.erb -->
<%= @form %>
Error Handling
ActionForm integrates with Rails validation errors:
class UserForm < ActionForm::Rails::Base
resource_model User
element :email do
input type: :email
output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
end
end
# When validation fails:
@form = @form.with_params(invalid_params)
# The form will automatically display validation errors
Rails-Specific Features
Automatic Form Attributes:
- CSRF protection with authenticity tokens
- UTF-8 encoding
- Proper HTTP methods (POST/PATCH)
- Rails form helpers integration
Model Integration:
- Automatic
persisted?
checks - Model name and param key handling
- Polymorphic path generation
Nested Attributes Support:
- Automatic
_attributes
parameter scoping - Primary key handling for existing records
- Delete flag handling for record removal
Dynamic Form Buttons
ActionForm provides built-in methods for rendering add/remove buttons for dynamic many
forms:
render_new_subform_button
- Renders a button to add new subform instances:
class UserForm < ActionForm::Rails::Base
resource_model User
many :addresses do
subform do
element :street do
input type: :text
output type: :string, presence: true
end
element :city do
input type: :text
output type: :string, presence: true
end
end
end
# Custom rendering with add button
def render_many_subforms(subforms)
super # Renders existing subforms and JavaScript
# Add a button to create new subforms
div(class: "form-actions") do
render_new_subform_button(class: "btn btn-primary") do
"Add Address"
end
end
end
end
render_remove_subform_button
- Renders a button to remove subform instances:
class UserForm < ActionForm::Rails::Base
resource_model User
many :addresses do
subform do
element :street do
input type: :text
output type: :string, presence: true
end
element :city do
input type: :text
output type: :string, presence: true
end
end
end
# Custom subform rendering with remove button
def render_subform(subform)
div(class: "address-form") do
super # Render the subform elements
# Add remove button for each subform
div(class: "form-actions") do
render_remove_subform_button(class: "btn btn-danger btn-sm") do
"Remove Address"
end
end
end
end
end
Complete Dynamic Form Example:
class UserForm < ActionForm::Rails::Base
resource_model User
many :addresses do
element :street do
input type: :text, class: "form-control"
output type: :string, presence: true
end
element :city do
input type: :text, class: "form-control"
output type: :string, presence: true
end
element :zip_code do
input type: :text, class: "form-control"
output type: :string, presence: true
end
end
# Custom rendering with both add and remove buttons
def render_many_subforms(subforms)
super
# Add button to create new subforms
div(class: "add-address-section") do
render_new_subform_button(
class: "btn btn-success",
data: { insert_before_selector: ".add-address-section" }
) do
span(class: "glyphicon glyphicon-plus") { }
" Add Address"
end
end
end
private
def render_subform(subform)
div(class: "address-form border p-3 mb-3") do
# Render subform elements
super
# Remove button
div(class: "form-actions text-right") do
render_remove_subform_button(
class: "btn btn-outline-danger btn-sm"
) do
span(class: "glyphicon glyphicon-trash") { }
" Remove"
end
end
end
end
end
Button Customization:
Both methods accept HTML attributes and blocks for complete customization:
# Custom styling and attributes
render_new_subform_button(
class: "btn btn-primary btn-lg",
id: "add-address-btn",
data: {
insert_before_selector: ".address-list",
confirm: "Add a new address?"
}
) do
icon("plus") + " Add New Address"
end
render_remove_subform_button(
class: "btn btn-danger btn-sm",
data: {
confirm: "Are you sure you want to remove this address?",
method: "delete"
}
) do
icon("trash") + " Remove"
end
JavaScript Integration:
The buttons automatically integrate with ActionForm's JavaScript functions:
-
easyFormAddSubform(event)
- Adds new subform instances -
easyFormRemoveSubform(event)
- Removes or marks subforms for deletion
The JavaScript handles:
- Template cloning with unique IDs
- Proper form field naming
- Delete flag setting for existing records
- DOM manipulation for dynamic forms
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/andriy-baran/action_form. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActionForm project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.