Skip to content

Rails Forms Deep — Advanced Form Building in Rails

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Rails Forms Deep. We cover key concepts, practical examples, and best practices to help you master this topic.

Rails forms use form_with for model-backed and standalone forms, supporting nested attributes, collection helpers, custom form builders, file uploads with Active Storage, and remote submissions.

What You'll Learn

By the end of this tutorial, you'll build model-backed forms, handle nested attributes for associations, use collection helpers, create custom form builders, and implement file uploads.

Why Forms Matter

Forms are the primary way users create and edit data. Rails form helpers handle CSRF protection, error display, and association forms automatically.

Real-World Use

A product form includes nested fields for variants, uses collection select for categories, Active Storage for images, and a custom form Builder for consistent Bootstrap styling.

Forms Path

flowchart LR
  A[Rails Views] --> B[Forms Deep]
  B --> C[form_with]
  B --> D[Nested Forms]
  B --> E[Collection]
  B --> F[Custom Builders]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Model-Backed Forms

Use form_with with a model.

<%= form_with model: @product, local: true, class: "product-form" do |form| %>
  <% if @product.errors.any? %>
    <div class="error-messages">
      <h2><%= pluralize(@product.errors.count, "error") %> prevented saving:</h2>
      <ul>
        <% @product.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name, placeholder: "Product name", required: true %>
  </div>
  <div class="field">
    <%= form.label :price %>
    <%= form.number_field :price, step: 0.01, min: 0 %>
  </div>
  <div class="field">
    <%= form.label :category_id %>
    <%= form.collection_select :category_id, Category.order(:name), :id, :name, prompt: "Select category" %>
  </div>
  <div class="field">
    <%= form.label :published %>
    <%= form.check_box :published %>
  </div>
  <div class="actions">
    <%= form.submit class: "btn btn-primary" %>
    <%= link_to "Cancel", products_path, class: "btn btn-secondary" %>
  </div>
<% end %>

Nested Attributes

Build nested forms for associations.

class Product < ApplicationRecord
  has_many :variants, dependent: :destroy
  accepts_nested_attributes_for :variants, allow_destroy: true, reject_if: :all_blank
end
<%= form_with model: @product do |form| %>
  <%= form.fields_for :variants do |variant_form| %>
    <div class="nested-fields">
      <%= variant_form.label :name %>
      <%= variant_form.text_field :name %>
      <%= variant_form.label :sku %>
      <%= variant_form.text_field :sku %>
      <%= variant_form.label :price %>
      <%= variant_form.number_field :price, step: 0.01 %>
      <% if variant_form.object.persisted? %>
        <%= variant_form.label :_destroy, "Remove" %>
        <%= variant_form.check_box :_destroy %>
      <% end %>
    </div>
  <% end %>
  <%= link_to_add_fields "Add Variant", form, :variants %>
<% end %>

Custom Form Builder

Create reusable form builders.

# app/helpers/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(method, options = {})
    super(method, options.merge(class: "form-control #{'is-invalid' if @object.errors[method].any?}"))
  end

  def label(method, text = nil, options = {}, &block)
    super(method, text, options.merge(class: "form-label"), &block)
  end

  def submit(text = nil, options = {})
    super(text, options.merge(class: "btn btn-primary"))
  end

  def error_messages
    return "" unless @object.errors.any?
    @object.errors.full_messages.map { |msg| content_tag(:div, msg, class: "alert alert-danger") }.join.html_safe
  end
end

Collection Helpers

Render collection-based form controls.

<!-- Collection select -->
<%= form.collection_select :category_id, Category.all, :id, :name, { prompt: "Choose..." }, { class: "form-select" } %>

<!-- Collection radio buttons -->
<%= form.collection_radio_buttons :category_id, Category.active, :id, :name do |b| %>
  <div class="radio-option">
    <%= b.radio_button %>
    <%= b.label %>
  </div>
<% end %>

<!-- Collection checkboxes for has_many -->
<%= form.collection_check_boxes :tag_ids, Tag.all, :id, :name do |b| %>
  <div class="checkbox-option">
    <%= b.check_box %>
    <%= b.label %>
  </div>
<% end %>

Common Mistakes

1. Not Using Model-Backed Forms

form_with model: @product handles CSRF, error display, and field population automatically.

2. Missing allow_destroy for Nested Forms

accepts_nested_attributes_for without allow_destroy prevents removing nested records.

3. Rejecting Nested Attributes Incorrectly

reject_if: :all_blank rejects if all fields are blank. Customize with a Proc.

4. Not Handling File Upload Errors

File validations need both model validations and frontend checks.

5. Forgetting Local: True for Non-Turbo Forms

Turbo handles forms by default. Use local: true to bypass Turbo.

Practice Questions

1. What does accepts_nested_attributes_for do?

Allows creating and updating associated records through the parent form.

2. How do you add a collection select?

Use form.collection_select :category_id, Category.all, :id, :name.

3. What is a custom form builder?

A class inheriting from ActionView::Helpers::FormBuilder that customizes form rendering.

4. How do you allow deleting nested records?

Add allow_destroy: true to accepts_nested_attributes_for and a _destroy checkbox.

5. Challenge: Create a product form with nested variant fields.

<%= form_with model: @product do |f| %>
  <%= f.text_field :name %>
  <%= f.fields_for :variants do |vf| %>
    <%= vf.text_field :name %>
    <%= vf.number_field :price %>
  <% end %>
  <%= f.submit %>
<% end %>

FAQ

What is the difference between form_for and form_with?

form_with is the modern replacement. form_for is deprecated.

How do I add a date picker?

Use form.date_field :published_at for a native date picker.

Can I use forms without models?

Yes. Use form_with url: '/search', method: :get.

What is fields_for?

Renders nested form fields for an association.

How do I display validation errors in forms?

Check @model.errors.any? and display error messages.

Mini Project: Product Form with Nested Variants

Build a complete product form with nested variant management.

<%= form_with model: @product, builder: BootstrapFormBuilder do |f| %>
  <%= f.error_messages %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <h3>Variants</h3>
  <%= f.fields_for :variants do |vf| %>
    <%= vf.text_field :name %>
    <%= vf.text_field :sku %>
    <%= vf.number_field :price %>
    <%= vf.check_box :_destroy %> Remove
  <% end %>
  <%= f.submit %>
<% end %>

What's Next

Rails Authentication Devise Deep Rails Cancancan Rails API Mode

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro