Railsamples

Practical examples to master web forms in Rails

Nested Attributes - has many association

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)