Railsamples

Practical examples to master web forms in Rails

Nested Attributes - Custom mark_for_destruction

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)