Practical examples to master web forms in Rails
This example shows how to use `#mark_for_destruction` method on a nested attributes to filter the records to create through a `:selected` checkbox. We're creating a user with hobbies based on a set list of activities. Unlike the collection checkbox helper in Rails, user hobbies hold a required preference input per hobby so it cannot be a simple join table. The hobby preference represents the number of hours a user wants to practice the activity in a week. Note the `build_missing_hobby` in the controller to build possible missing hobbies on the form. Any unselected hobby gets discarded or deleted on submission.
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.0"
gem "sqlite3", "~> 1.7"
gem "byebug"
gem "puma"
end
require "uni_rails"
require "sqlite3"
require "byebug"
require 'rack/handler/puma'
UniRails.rackup_handler = Rack::Handler::Puma
# ==== ROUTES ====
UniRails::App.routes.append do
root "users#new"
resources :users
end
# ==== DB SCHEMA ====
ActiveRecord::Base.establish_connection
ActiveRecord::Schema.define do
create_table :users, force: :cascade do |t|
t.string :name
end
create_table :activities, force: :cascade do |t|
t.string :name
end
create_table :hobbies, force: :cascade do |t|
t.references :activity
t.references :user
t.decimal :preference
t.index [:activity_id, :user_id], unique: true
end
end
# ==== MODELS ====
class User < ActiveRecord::Base
has_many :hobbies
accepts_nested_attributes_for :hobbies, allow_destroy: true
validates :name, presence: true
def hobbies_attributes=(attributes =[])
super(attributes)
hobbies.each do |hobby|
next if hobby.selected?
hobby.mark_for_destruction
end
end
end
class Activity < ActiveRecord::Base
end
class Hobby < ActiveRecord::Base
belongs_to :activity
belongs_to :user
attribute :selected, :boolean, default: true
validates :preference, presence: true, if: :selected?
delegate :name, to: :activity
end
# ==== SEEDS ====
football = Activity.create(name: 'Football')
tennis = Activity.create(name: 'Tennis')
swimming = Activity.create(name: 'Swimming')
painting = Activity.create(name: 'Painting')
sculpture = Activity.create(name: 'Sculpture')
User.create(name: 'Alex', hobbies_attributes: [{ activity: football, preference: 10 }])
# ==== CONTROLLERS ====
class UsersController < ActionController::Base
layout 'application'
def show
@user = User.find(params[:id])
end
def new
@user = User.new
build_missing_hobbies(@user)
end
def create
@user = User.new(user_params)
if @user.save
redirect_to user_path(@user)
else
render :new
end
end
def edit
@user = User.find(params[:id])
build_missing_hobbies(@user)
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to user_path(@user)
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, hobbies_attributes: [:id, :selected, :activity_id, :preference])
end
def build_missing_hobbies(user)
Activity.where.not(id: user.hobbies.select(:activity_id)).find_each do |activity|
user.hobbies.build(activity: activity, selected: false)
end
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("hello", class extends Controller {
connect() {
console.log("hello world")
}
})
JAVASCRIPT
# ==== CSS ====
UniRails.css <<~CSS
html { background-color:#EEE; }
body { width:500px; height:700px; margin:auto;
background-color:white; padding:1rem;
}
form {
label { display: block; }
input[type="submit"] { display: block; margin-top:1rem; }
.field_with_errors { color:red; display:inline;
.without-label { background-color: rgba(255, 0, 0, 0.5); }
}
}
CSS
# ==== VIEWS ====
UniRails.register_view "users/_form.html.erb", <<~HTML
<%= form_with model: user do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<fieldset>
<legend>Hobbies</legend>
<table>
<thead>
<tr>
<th>Selected</th>
<th>Hobby</th>
<th>Preference (hours practiced)</th>
</tr>
</thead>
<tbody>
<%= f.fields_for :hobbies do |ff| %>
<%= ff.hidden_field :activity_id %>
<%= ff.hidden_field :id %>
<tr>
<td><%= ff.check_box :selected %></td>
<td><%= ff.object.name %></td>
<td><%= ff.number_field :preference, class: 'without-label' %></td>
</tr>
<% end %>
</tbody>
</table>
</fieldset>
<%= f.submit %>
<% end %>
HTML
UniRails.register_view "users/new.html.erb", <<~HTML
<h1>New User</h1>
<%= render partial: 'form', locals: { user: @user } %>
HTML
UniRails.register_view "users/show.html.erb", <<~HTML
<h1><%= @user.name.capitalize %></h1>
<p><%= link_to 'Edit', edit_user_path(@user) %></p>
<h2>Hobbies</h2>
<ul>
<% @user.hobbies.includes(:activity).find_each do |hobby| %>
<li><%= hobby.name %> - (Preference <%= hobby.preference %>)</li>
<% end %>
</ul>
HTML
UniRails.register_view "users/edit.html.erb", <<~HTML
<h1><%= @user.name.capitalize %></h1>
<%= render partial: 'form', locals: { user: @user } %>
HTML
UniRails.run(Port: 3000)