Practical examples to master web forms in Rails
This example demonstrates how to create a nested form with a 'has many' association. Pay attention to the Stimulus controller used and how the form fields are reused. Note also how the fields_for are defined on both the form and the template.
ENV["SECRET_KEY_BASE"] = "1212312313"
ENV["DATABASE_URL"] = "sqlite3:///#{__dir__}/database.sqlite"
require "bundler/inline"
gemfile do
source "https://www.rubygems.org"
gem "uni_rails", "0.4.1"
gem "sqlite3", "~> 1.7"
gem "byebug"
end
require "uni_rails"
require "sqlite3"
require "byebug"
# ==== ROUTES ====
UniRails::App.routes.append do
root "accounts#new"
resources :accounts
end
# ==== DB SCHEMA ====
ActiveRecord::Base.establish_connection
ActiveRecord::Schema.define do
create_table :accounts, force: :cascade do |t|
t.string :name, null: false
end
create_table :transactions, force: :cascade do |t|
t.references :account
t.string :category
t.decimal :value, null: false
end
end
# ==== MODELS ====
class Account < ActiveRecord::Base
has_many :transactions
accepts_nested_attributes_for :transactions, allow_destroy: true
validates :name, presence: true
def transactions_total
transactions.sum(&:value)
end
end
class Transaction < ActiveRecord::Base
CATEGORIES = %w[housing food bills transportation other]
belongs_to :account
validates :value, presence: true, numericality: true
before_validation { self.category = self.category.presence || 'other' }
end
# ==== CONTROLLERS ====
class AccountsController < ActionController::Base
layout 'application'
def new
@account = Account.new transactions: [
Transaction.new(value: 100, category: 'food'),
Transaction.new(value: 100, category: 'housing'),
Transaction.new(value: 100, category: 'transportation'),
Transaction.new(value: 100, category: 'bills'),
Transaction.new(value: 100, category: 'other'),
]
end
def create
if (@account = Account.new(account_params)).save
redirect_to @account
else
render :new
end
end
def show
@account = Account.find(params[:id])
end
def edit
@account = Account.find(params[:id])
end
def update
if (@account = Account.find(params[:id])).update(account_params)
redirect_to @account
else
render :edit
end
end
private
def account_params
params
.require(:account)
.permit(:name, transactions_attributes: [:_destroy, :id, :category, :value])
end
end
# ==== IMPORT MAPS ====
UniRails.import_maps(
'stimulus' => 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js'
)
# ==== JAVASCRTIP ====
UniRails.javascript <<~JAVASCRIPT
import { Application, Controller } from "stimulus"
window.Stimulus = Application.start()
Stimulus.register("nested-attributes", class extends Controller {
static targets = ["template", "container"]
addNestedForm(event) {
event.preventDefault();
const uniqueKey = Date.now()
this.containerTarget.insertAdjacentHTML(
"beforeend",
this.templateTarget.innerHTML.replaceAll(/X-ID/g, uniqueKey)
)
}
removeNestedForm(event) {
event.preventDefault();
var element = event.target.parentElement
element.style.display = 'none'
element.querySelector("input[name*='_destroy']").value = '1'
}
})
JAVASCRIPT
# ==== CSS ====
UniRails.css <<~CSS
html { background-color:#EEE; }
body { width:500px; height:700px; margin:auto;
background-color:white; padding:1rem;
}
fieldset { width: fit-content; margin-top: 1rem; }
table, th, td { border: 1px solid; }
th, td { padding: 0.5rem; }
form {
.errors { color: red; }
label { display: block; }
input[type="submit"] { display: block; margin-top:1rem; }
.field_with_errors { color:red; display:inline; }
.transaction { display: flex; margin-bottom: 0.5rem;
label, input, select { margin-right: 1rem; }
}
}
CSS
# ==== VIEWS ====
UniRails.register_view "accounts/new.html.erb", <<~HTML
<h1>New Account</h1>
<%= render 'form', account: @account %>
HTML
UniRails.register_view "accounts/edit.html.erb", <<~HTML
<h1>Edit Account <%= @account.name %></h1>
<%= render 'form', account: @account %>
HTML
UniRails.register_view "accounts/show.html.erb", <<~HTML
<h1><%= @account.name %> details</h1>
<p><%= link_to 'Edit account', edit_account_path(@account) %></p>
<table id="transactions_totals">
<thead>
<th>Category</th>
<th>Total</th>
</thead>
<tbody>
<% @account.transactions.group_by(&:category).each do |category, transactions| %>
<tr>
<td><%= category.upcase %></td>
<td><%= number_to_currency transactions.sum(&:value) %></td>
</tr>
<% end %>
<tr>
<td><strong>Total</strong></td>
<td><%= number_to_currency @account.transactions_total %></td>
</tr>
</tbody>
</table>
HTML
UniRails.register_view "accounts/_form.html.erb", <<~HTML
<%= form_with model: account do |f| %>
<p class="errors"><%= account.errors.full_messages.to_sentence %></p>
<%= f.label :name %>
<%= f.text_field :name %>
<fieldset data-controller="nested-attributes">
<legend>Transactions</legend>
<div id="transactions" data-nested-attributes-target="container">
<%= f.fields_for :transactions, account.transactions do |ff| %>
<%= render 'transaction_fields', f: ff do %>
<%= link_to 'remove', '#', data: { action: 'nested-attributes#removeNestedForm' } %>
<% end %>
<% end %>
</div>
<div><%= link_to 'Add transaction', '#', data: { action: "nested-attributes#addNestedForm" } %> </div>
<template data-nested-attributes-target="template">
<%= f.fields_for :transactions, Transaction.new, child_index: 'X-ID' do |ff| %>
<%= render 'transaction_fields', f: ff do %>
<%= link_to 'remove', '#', data: { action: 'nested-attributes#removeNestedForm' } %>
<% end %>
<% end %>
</template>
</fieldset>
<%= f.submit %>
<% end %>
HTML
UniRails.register_view "accounts/_transaction_fields.html.erb", <<~HTML
<div class="transaction" style="<%= 'display:none;' if f.object.marked_for_destruction? %>">
<%= f.hidden_field :id %>
<%= f.hidden_field :_destroy %>
<%= f.label :value %>
<%= f.number_field :value, step: 0.01 %>
<%= f.label :category %>
<%= f.collection_select :category, Transaction::CATEGORIES, :to_s, :to_s, include_blank: true %>
<%= yield %>
</div>
HTML
UniRails.run(Port: 3000)