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
- Custom parameter validation with the
paramsmethod - 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 installOr install it yourself as:
$ gem install action_formRequirements
- 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, andmanyto 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, andmanymethods -
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 :nameexpects the object to have anamemethod) - 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
outputconfiguration determines how submitted data is processed
Key Features
- Declarative DSL: Define forms with simple, readable syntax
-
Nested Forms: Support for complex nested structures with
subformandmany -
Custom Parameter Validation: Use the
paramsmethod to add custom validation logic and schema modifications - 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,manymethods) - 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
-
Custom Parameter Validation: Use the
paramsmethod to add custom validation logic and schema modifications - Error Integration: Failed validations can re-render the form with submitted data and error messages
-
Nested Support: Both phases support complex nested structures through
subformandmanyrelationships
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
endAvailable 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"
endNested 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
endUse 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
endComplete 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
endCustom Parameter Validation
ActionForm provides a params method that allows you to add custom validation logic and schema modifications to your form's parameter handling. This is particularly useful for complex validation rules that depend on context or require cross-field validation.
Basic Usage
Use the params method to define custom validation logic:
class UserForm < ActionForm::Base
element :email do
input type: :email
output type: :string, presence: true
end
element :password do
input type: :password
output type: :string
end
element :password_confirmation do
input type: :password
output type: :string
end
# Custom parameter validation
params do
validates :password, presence: true
validates :password_confirmation, presence: true
validates :password, confirmation: true
end
endConditional Validation
You can add conditional validation logic based on context:
class UserForm < ActionForm::Base
element :email do
input type: :email
output type: :string, presence: true
end
element :password do
input type: :password
output type: :string
end
# Conditional validation based on external context
params do
secure_mode = true # This could come from configuration or request context
validates :password, presence: true, if: -> { secure_mode }
validates :password, length: { minimum: 8 }, if: -> { secure_mode }
end
endNested Form Validation
The params method also supports validation for nested forms using schema blocks:
class UserForm < ActionForm::Base
element :email do
input type: :email
output type: :string, presence: true
end
subform :profile, default: {} do
element :name do
input type: :text
output type: :string
end
end
many :addresses, default: [{}] do
subform do
element :street do
input type: :text
output type: :string
end
element :city do
input type: :text
output type: :string
end
end
end
params do
# Validate nested subform
profile_attributes_schema do
validates :name, presence: true
end
# Validate nested collection
addresses_attributes_schema do
validates :street, presence: true
validates :city, presence: true
end
end
endDynamic Form Classes
You can create dynamic form classes with different validation rules:
class BaseUserForm < ActionForm::Base
element :email do
input type: :email
output type: :string, presence: true
end
element :password do
input type: :password
output type: :string
end
end
# Create a secure version with additional validation
SecureUserForm = Class.new(BaseUserForm)
SecureUserForm.params do
validates :password, presence: true, length: { minimum: 8 }
validates :password, format: { with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/ }
end
# Create a basic version with minimal validation
BasicUserForm = Class.new(BaseUserForm)
BasicUserForm.params do
validates :password, presence: true
endIntegration with Controllers
The custom parameter validation integrates seamlessly with your controllers:
class UsersController < ApplicationController
def create
user_params = UserForm.params_definition.new(params)
if user_params.valid?
@user = User.create!(user_params.to_h)
redirect_to @user
else
@form = user_params.creare_form
render :new
end
end
endThe params method provides a powerful way to extend ActionForm's parameter handling with custom validation logic while maintaining the declarative nature of form definition.
Modifying Element Definitions
ActionForm allows you to modify existing element, subform, and many definitions after they have been declared. This feature enables you to extend or customize form definitions without altering the original class structure, making it perfect for conditional modifications, inheritance patterns, and dynamic form customization.
Modifying Elements
Use the {element_name}_element method to modify an existing element definition:
class UserForm < ActionForm::Base
element :email do
input type: :email
output type: :string
end
element :password do
input type: :password
output type: :string
end
end
# Modify existing element definitions
UserForm.email_element do
output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
end
UserForm.password_element do
output type: :string, presence: true, length: { minimum: 8 }
endModifying Subforms
Use the {subform_name}_subform method to modify an existing subform definition:
class UserForm < ActionForm::Base
subform :profile do
element :bio do
input type: :textarea
output type: :string
end
end
end
# Modify the subform to add validation or change defaults
UserForm.profile_subform default: {} do
bio_element do
output type: :string, presence: true
end
# You can also add new elements
element :avatar do
input type: :file
output type: :string
end
endModifying Many Relationships
Use the {many_name}_subforms method to modify an existing many definition:
class OrderForm < ActionForm::Base
many :items do
subform do
element :name do
input type: :text
output type: :string
end
element :quantity do
input type: :number
output type: :integer
end
end
end
end
# Modify the many relationship to add validation or change defaults
OrderForm.items_subforms default: [{}] do
subform do
name_element do
output type: :string, presence: true
end
quantity_element do
output type: :integer, presence: true, inclusion: { in: 1..100 }
end
# Add new elements to existing many subforms
element :price do
input type: :number
output type: :float, presence: true
end
end
endInheritance and Modifications
Element modifications work seamlessly with class inheritance:
class BaseForm < ActionForm::Base
element :name do
input type: :text
output type: :string
end
end
class UserForm < BaseForm
element :email do
input type: :email
output type: :string
end
end
# Modify inherited elements
UserForm.name_element do
output type: :string, presence: true
end
# Modify elements defined in subclass
UserForm.email_element do
output type: :string, presence: true
endHere's a complete example showing element modifications in action:
class OrderForm < ActionForm::Base
element :name do
input(type: :text)
output(type: :string)
end
subform :customer do
element :name do
input(type: :text)
output(type: :string)
end
end
many :items do
subform do
element :name do
input(type: :text)
output(type: :string)
end
element :quantity do
input(type: :number)
output(type: :integer)
end
element :price do
input(type: :number)
output(type: :float)
end
end
end
end
# Apply modifications to add validation
secure = true
OrderForm.name_element do
output(type: :string, presence: true, if: -> { secure })
end
OrderForm.customer_subform default: {} do
name_element do
output(type: :string, presence: true, if: -> { secure })
end
end
OrderForm.items_subforms default: [{}] do
subform do
name_element do
output(type: :string, presence: true, if: -> { secure })
end
quantity_element do
output(type: :integer, presence: true, if: -> { secure })
end
price_element do
output(type: :float, presence: true, if: -> { secure })
end
end
end
# Now the form has validation enabled
params = OrderForm.params_definition.new({})
expect(params).to be_invalidKey Benefits
- Non-Destructive: Modify form definitions without changing the original class
- Conditional Logic: Apply modifications based on runtime conditions
- Inheritance Support: Works seamlessly with class inheritance
- Flexible Extension: Add validation, change defaults, or add new elements to existing definitions
- Reusability: Create base forms and customize them for specific use cases
This feature provides a powerful way to customize and extend form definitions while maintaining the declarative nature of ActionForm.
Composition and Owner Delegation
ActionForm includes a composition system that allows form components (elements, subforms, and many collections) to access methods from their owner (typically the parent form or a custom host object). This promotes code reuse and reduces redundancy by allowing shared logic to be defined in a central location and accessed by multiple form components.
How Composition Works
When you create a form, all nested elements and subforms automatically have access to their owner through the owner accessor. To delegate methods to the owner, you must prefix method calls with owner_. The owner_ prefix is automatically stripped, and the method is then searched for on the ownership chain, allowing you to access methods from the parent form or a custom host object.
Automatic Ownership Chain
Ownership is automatically established when forms are built:
class ProductForm < ActionForm::Base
element :name do
input(type: :text)
output(type: :string)
# Use owner_ prefix to delegate to owner's method
def render?
owner_name_render? # Delegates to owner.name_render?
end
end
many :variants, default: [{}] do
subform do
element :name do
input(type: :text)
output(type: :string)
def render?
owner_variants_name_render? # Delegates to owner.variants_name_render?
end
end
def render?
owner_variants_subform_render? # Delegates to owner.variants_subform_render?
end
end
end
subform :manufacturer, default: {} do
element :name do
input(type: :text)
output(type: :string)
def render?
owner_manufacturer_name_render? # Delegates to owner.manufacturer_name_render?
end
end
end
# Methods accessible by nested components via owner_ prefix
def name_render?
true
end
def variants_name_render?
true
end
def variants_subform_render?
true
end
def manufacturer_name_render?
true
end
endCustom Owner Object
You can pass a custom owner object when initializing a form, allowing you to separate form logic from business logic:
class HostObject
def name_render?
current_user.admin? || form_context == :edit
end
def variants_subform_render?
feature_enabled?(:variants)
end
def variants_name_render?
true
end
def variants_price_render?
pricing_enabled?
end
def manufacturer_name_render?
manufacturer_feature_enabled?
end
end
class ProductForm < ActionForm::Base
element :name do
input(type: :text)
output(type: :string)
def render?
owner_name_render? # Calls HostObject#name_render? via owner_ prefix
end
end
many :variants, default: [{}] do
subform do
element :name do
input(type: :text)
output(type: :string)
def render?
owner_variants_name_render? # Calls HostObject#variants_name_render?
end
end
element :price do
input(type: :number)
output(type: :float)
def render?
owner_variants_price_render? # Calls HostObject#variants_price_render?
end
end
def render?
owner_variants_subform_render? # Calls HostObject#variants_subform_render?
end
end
end
subform :manufacturer, default: {} do
element :name do
input(type: :text)
output(type: :string)
def render?
owner_manufacturer_name_render? # Calls HostObject#manufacturer_name_render?
end
end
end
end
# Use the form with a custom host object
host = HostObject.new
product = Product.new(name: "Product 1")
form = ProductForm.new(object: product, owner: host)Ownership Chain Traversal
The composition system supports multi-level ownership chains. When a method is called with the owner_ prefix on an element or subform, it searches up the ownership chain until it finds the method:
class GrandparentForm < ActionForm::Base
def shared_helper
"grandparent"
end
end
class ParentForm < ActionForm::Base
def shared_helper
"parent"
end
end
class ChildForm < ActionForm::Base
element :field do
input(type: :text)
def render?
owner_shared_helper # Will find ParentForm#shared_helper first
end
end
end
# If ChildForm has ParentForm as owner, and ParentForm has GrandparentForm as owner:
# The method lookup order is: ParentForm -> GrandparentForm
# Note: Methods must be prefixed with owner_ to trigger delegationAccessing Owner Directly
You can access the owner directly using the owner accessor. However, for delegation, it's recommended to use the owner_ prefix pattern:
class ProductForm < ActionForm::Base
element :discount_code do
input(type: :text)
output(type: :string)
def disabled?
# Preferred: Use owner_ prefix for delegation
owner_is_discount_disabled?
# Alternative: Direct access via owner accessor (requires owner to be set)
# owner.current_user && !owner.current_user.admin?
end
def placeholder
# Preferred: Use owner_ prefix
owner_discount_placeholder_text
# Alternative: Direct access
# owner.discount_placeholder_text
end
end
def is_discount_disabled?
current_user && !current_user.admin?
end
def current_user
@current_user ||= User.find(session[:user_id])
end
def discount_placeholder_text
"Enter discount code"
end
endWhen to use each approach:
-
owner_method_name: Recommended for delegation - automatically searches the ownership chain -
owner.method_name: Useful when different owners in the chain define the same method. By default, the nearest owner will handle the call. If you want to customize which owner is used, consider usingalias_methodor explicitly referencing a higher owner (e.g.,owner.owner.some_method).
Ownership Hierarchy
The ownership hierarchy is automatically established as follows:
-
Top-level form: Can have a custom
ownerpassed during initialization - Elements: Owner is the form or subform that contains them
- Subforms: Owner is the form that contains them
- SubformsCollection (many): Owner is the form that contains them
- Nested elements in subforms: Owner is the subform that contains them
- Nested subforms: Owner is the parent form or subform
Practical Use Cases
Conditional Rendering Based on Context:
class OrderForm < ActionForm::Base
element :admin_notes do
input(type: :textarea)
output(type: :string)
def render?
# Use owner_ prefix to delegate to owner method
owner_can_render_admin_notes?
end
end
def can_render_admin_notes?
current_user&.admin?
end
def current_user
@current_user
end
endShared Validation Logic:
class RegistrationForm < ActionForm::Base
element :email do
input(type: :email)
output(type: :string)
def disabled?
# Delegate to owner's email_locked? method
owner_email_locked?
end
end
element :email_confirmation do
input(type: :email)
output(type: :string)
def disabled?
owner_email_locked?
end
end
def email_locked?
@user.persisted? && @user.email_verified?
end
endFeature Flags:
class SettingsForm < ActionForm::Base
many :advanced_settings do
subform do
element :feature_flag do
input(type: :checkbox)
output(type: :bool)
def render?
# Delegate with owner_ prefix
owner_feature_enabled?(:advanced_settings)
end
end
end
def render?
owner_feature_enabled?(:advanced_settings)
end
end
def feature_enabled?(feature)
FeatureFlags.enabled?(feature, current_user)
end
endBenefits
- Code Reuse: Share common logic across multiple form components
- Separation of Concerns: Keep business logic in host objects, form logic in forms
- Flexibility: Support conditional rendering and behavior based on context
- Maintainability: Centralize shared logic instead of duplicating it
- Testability: Test host objects separately from form definitions
The composition system provides a powerful mechanism for creating flexible, maintainable forms that can adapt to different contexts and requirements.
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"
endTag 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
endNested 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
endCustom 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
endThe 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
endBootstrap-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
endConditional 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
endCustom 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
endCustom 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
endComplete 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
endAdvanced 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
endDynamic 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
endThe 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
endhtml_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
endrender? - 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
enddetached? - 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
endLabel 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'
enddisplay: false - Label won't be rendered
element :full_name do
input type: :text
label display: false
endElement Properties
name - The element's name (symbol):
element :username do
input type: :text
end
# Access the name:
element.name # => :usernametags - 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
endreadonly? - Controls whether the element is readonly:
element :email do
input type: :email
def readonly?
object.verified? # Readonly if email is verified
end
endElement 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
endRails 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 :email do
input type: :email
output type: :string, presence: true
end
element :password do
input type: :password
output type: :string
end
element :password_confirmation do
input type: :password
output type: :string
end
subform :profile, default: {} do
element :name do
input(type: :text)
output(type: :string)
end
params do
validates :name, presence: { if: :owner_check_profile_name? }
end
end
many :devices, default: [{}] do
subform do
element :name do
input(type: :text)
output(type: :string)
end
params do
validates :name, presence: { if: :owner_check_devices_name? }
end
end
end
# Custom parameter validation for Rails integration
params do
validates :password, presence: true, length: { minimum: 6 }
validates :password, confirmation: true
# You can customize nested schema from example above like this
profile_attributes_schema do
validates :name, presence: { if: :owner_check_profile_name? }
end
devices_attributes_schema do
validates :name, presence: { if: :owner_check_devices_name? }
end
end
endModel 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
endParameter 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]
endNested 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
endAutomatic Features:
- Primary key elements (
id) are automatically added for existing records - Delete elements (
_destroy) are automatically added for removal - Parameters are properly scoped with
_attributessuffix - 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
There is a separate gem for Rails integration [steel_wheel|https://github.com/andriy-baran/steel_wheel]
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
# Custom validation errors are automatically available
@form = user_params.create_form(action: request.path, method: request.method)
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
# Custom validation errors (like password confirmation) are displayed
@form = user_params.create_form(action: request.path, method: request.method)
render :edit
end
end
endView 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 = user_params.create_form
# The form will automatically display validation errorsRails-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
_attributesparameter 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
endrender_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
endComplete 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
endButton 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"
endJavaScript 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.