Create multiple records simultaneously on one form - ruby-on-rails

After trying to accomplish the deceptively tricky task of building a form that enables the creation of multiple users at once, I wanted to pay it forward by sharing how I did it. Somewhat to my surprise, I was unable to find any SO answers that addressed this problem. Instead, the questions/answers kept addressing the scenario of creating a model as well as its associations on one form (using nested_attributes).

I must attribute the answer to a Ryan Bates' post from 2007 http://archive.railsforum.com/viewtopic.php?id=2696.
Your controller
def new
#owners = Array.new(3) { Owner.new } # set up any defaults here
end
def create
#owners = params[:owners].values.collect { |owner| Owner.new(owner) }
if #owners.all?(&:valid?)
#owners.each(&:save!)
redirect_to :action => 'index'
else
render :action => 'new'
end
end
Your View - new.erb.html
<% form_tag :action => 'create' do %>
<% #owners.each_with_index do |owner, index| %>
<% fields_for "owners[#{index}]", owner do |f| %>
First Name: <%= f.text_field :firstname %>
Last Name: <%= f.text_field :lastname %>
<% end %>
<% end %>
<%= submit_tag %>
<% end %>

Related

How to edit a single attribute inline with Turbo Frame and Trubo Stream with validation feedback?

Creating In-Place-Editing of a single attribute of a model using Turbo Frames (not using a gem such as Best_In_Place as it requires jQuery and is not working well with Rails 7) This implemenation is using ONLY turboframes.
To accomplish this I followed this tutorial: https://nts.strzibny.name/single-attribute-in-place-editing-turbo/ (written in January 2022)
The tutorial does not match Ruby 3.2.0, Rails 7.0.4 perfectly and needs a one variable adjustment on the show page to work.
Unfortunately, there is no validation feedback currently in this tutorials method as the turbo_frame form implemented does not have it included.
Question: how to properly add validation feedback and routing of errors? (preferably a turbo_frames only solution)
Summary of tutorial:
create new app and scaffold one model: User name:string
changes to UsersController (a new action on the controller to edit a single attribute, and adding edit_name to before_action list)
before_action :set_user, only: %i[ show edit edit_name update destroy ]
# GET /users/1/edit_name
def edit_name
end
add to routes.rb (a new route for editing a single specific attribute)
resources :users do
member do
get 'edit_name'
end
end
create view/users/edit_name.html.erb (a new view page to support editing a specific attribute, (here a name)).
<%= turbo_frame_tag "name_#{user.id}" do %>
<%= form_with model: #user, url: user_path(#user) do |form| %>
<%= form.text_field :name %>
<%= form.submit "Save" %>
<% end %>
<% end %>
additions on _user.html.erb file (the link to the created turbo frame form edit_name.html.erb)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to #user.name, edit_name_user_path(#user) %>
<% end %>
Upon starter the app server I get errors about #user being nil:Class.
In order to get the tutorial to work I have to change the _user.html.erb file to use a local variable for user in the link.
edited again  _user.html.erb (changing instance variable #user to local variable user)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to user.name, edit_name_user_path(user) %>
<% end %>
With this change, the tutorial works, allowing single attribute in place editing through turbo frames! But no model validation feedback is implemented.
Below, I attempt to deal with validation, first adding validation to models/user.rb
class User < ApplicationRecord
validates :name, presence: true
validates :name, comparison: { other_than: "Jason" }
end
PROPOSED SOLUTION:
CREATE a new turbo_stream file for editing errors that pop up (it has an error in the turbo_frame tag that it is targeting, it needs to be able to target any parent turboframe where the single attribute edit was initiated)
<%= turbo_stream.replace"name_#{#user.id}" do %>
<%= form_with model: #user, url: user_path(#user) do |form| %>
<% if #user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(#user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% #user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<% if #user.errors[:name].any? %>
<%= form.label :name, style: "display: block" %> <%= form.text_field :name %>
<% end %>
<% if #user.errors[:active].any? %>
<%= form.label :active, style: "display: block" %> <%= form.check_box :active %>
<% end %>
<%= form.submit "Save" %>
<% end %>
<% end %>
and edit the UsersController.rb update method to deal with turbo stream errors
# PATCH/PUT /users/1 or /users/1.json
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to user_url(#user), notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: #user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: #user.errors, status: :unprocessable_entity }
format.turbo_stream do
if #user.errors[:name].any?
#user.name = nil #so that it does not repopulate the form with the bad data
if #user.errors[:active].any?
#user.active = nil
end
render :edit_errors, status: :unprocessable_entity
end
end
end
end
This all works except for after entering a succesful edit on the form produced after an invalid entry, it renders the show for that entry only, rather than all of them.
What would be a 'dry'er method of doing all of this? (and how do I target updating just the one frame from the turbo stream so that only the one field gets updated after success on validation)?
Philosophically, is any of this worth it now compared to just using jQuery and the Gem Best_In_Place??? Seems like the number of changes are piling up and the code will get ugly if supporting such functionality across multiple attributes?
Since the initial issue is resolved, I'll just add some other ways you can do this. It's gonna be a little more work to do this yourself and you won't have all the functionality that some gem could give you. On the other hand, it's a lot less code and you have full control over everything. Besides, if you just need to have this one field to be editable, installing a gem and jquery is too much overhead.
Setup:
# rails v7.0.4.2
$ rails new hello_edit_in_place -c tailwind
$ cd hello_edit_in_place
$ bin/rails g scaffold User email first_name last_name --skip-timestamps
$ bin/rails db:migrate
$ bin/rails runner "User.create(email: 'admin#localhost', first_name: 'super', last_name: 'admin')"
$ open http://localhost:3000/users
$ bin/dev
class User < ApplicationRecord
validates :email, presence: true, length: {minimum: 3}
end
Turbo Frame
I'll just modify the default form and won't touch the controller as a quick example:
# app/views/users/_form.html.erb
# NOTE: this lets you render this partial and pass a local `:attribute` or
# get attribute from url params.
<% if attribute ||= params[:attribute] %>
<%= turbo_frame_tag dom_id(user, attribute) do %>
# NOTE: send `attrtibute` back in case of validation error, so this page
# can be rendered again with params[:attribute] set.
# V
<%= form_with model: user, url: user_path(user, attribute:) do |f| %>
<%= f.text_field attribute %>
# NOTE: show validation errors
<%= safe_join user.errors.full_messages_for(attribute), tag.br %>
<%= f.submit "save" %>
<% end %>
<% end %>
<% else %>
# original form here
<% end %>
# app/views/users/_user.html.erb
# NOTE: there is no need to have the whole set up for each individual
# attribute
<% user.attribute_names.reject{|a| a =~ /^(id|something_else)$/}.each do |attribute| %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %> # tag.div - so that i can keep rb syntax highlight for stackoverflow
<%= turbo_frame_tag dom_id(user, attribute) do %>
<%= link_to edit_user_path(user, attribute:) do %>
<%= user.public_send(attribute).presence || "—".html_safe %>
<% end %>
<% end %>
<% end %>
That's it, every attribute is rendered, is editable and email shows validation errors. Also because all turbo_frame_tags have a unique id, everything works with multiple users on the index page.
Turbo Stream
You can also use turbo_stream to have more flexibility and make it even more dynamic, but it's a bit more of a set up. Also, add ability to edit full name in place, with first_name and last_name fields together:
# config/routes.rb
# NOTE: to not mess with default actions, add new routes
resources :users do
member do
get "edit_attribute/:attribute", action: :edit_attribute, as: :edit_attribute
patch "update_attribute/:attribute", action: :update_attribute, as: :update_attribute
end
end
# app/views/users/_user.html.erb
# Renders user attributes.
# Required locals: user.
<%= render "attribute", user:, attribute: :email %>
<%= render "attribute", user:, attribute: :name %>
# app/views/users/_attribute.html.erb
# Renders editable attribute.
# Required locals: attribute, user.
<%= tag.div id: dom_id(user, attribute) do %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %>
# NOTE: to make a GET turbo_stream request vvvvvvvvvvvvvvvvvvvvvvvvvv
<%= link_to edit_attribute_user_path(user, attribute:), data: {turbo_stream: true} do %>
# far from perfect, but gotta start somewhere
<% if user.attribute_names.include? attribute.to_s %>
<%= user.public_send(attribute) %>
<% else %>
# if user doesn't have provided attribute, try to render a partial
<%= render attribute.to_s, user: %>
<% end %>
<% end %>
<% end %>
# app/views/users/_name.html.erb
# Renders custom editable attribute value.
# Required locals: user.
<%= user.first_name %>
<%= user.last_name %>
# app/views/users/_edit_attribute.html.erb
# Renders editable attribute form.
# Required locals: attribute, user.
<%= form_with model: user, url: update_attribute_user_path(user, attribute:) do |f| %>
<% if user.attribute_names.include? attribute.to_s %>
<%= f.text_field attribute %>
<% else %>
# NOTE: same as before but with `_fields` suffix,
# so this requires `name_fields` partial.
<%= render "#{attribute}_fields", f: %>
<% end %>
<%= f.submit "save" %>
<% end %>
# app/views/users/_name_fields.html.erb
# Renders custom attribute form fields.
# Requires locals:
# f - form builder.
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
# app/controllers/users_controller.rb
# GET /users/:id/edit_attribute/:attribute
def edit_attribute
attribute = params[:attribute]
respond_to do |format|
format.turbo_stream do
# render form
render turbo_stream: turbo_stream.update(
helpers.dom_id(user, attribute),
partial: "edit_attribute",
locals: {user:, attribute:}
)
end
end
end
# PATCH /users/:id/update_attribute/:attribute
def update_attribute
attribute = params[:attribute]
attribute_id = helpers.dom_id(user, attribute)
respond_to do |format|
if user.update(user_params)
format.turbo_stream do
# render updated attribute
render turbo_stream: turbo_stream.replace(
attribute_id,
partial: "attribute",
locals: {user:, attribute:}
)
end
else
format.turbo_stream do
# render errors
render turbo_stream: turbo_stream.append(
attribute_id,
html: (
helpers.tag.div id: "#{attribute_id}_errors" do
# FIXME: doesn't render `first_name` `last_name` errors
helpers.safe_join user.errors.full_messages_for(attribute), helpers.tag.br
end
)
)
end
end
end
end
private
def user
#user ||= User.find(params[:id])
end

Creating multiple objects in a form Rails

So I have an interesting problem I'm working on. I am trying to create multiple objects of the same model in one view. I would like to display all the possible objects in my view, check boxes to select which ones to create, then submit and create all the corresponding objects.
Now the objects to select are gotten using an API request and returned in JSON format. The JSON is then displayed on the view for the user to select, then an array containing all the selected objects is sent back to the controller for creation.
Here is the relevant code that I've tried so far.
objects_controller.rb
def new
#possible_objects = <api call to get objs>
#objects = []
end
def create
params[:objects].each do |obj|
# create and save obj
end
end
objects/new.html.erb
<% form_for #objects do |f| %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag(api_obj["name"])%>
<%= api_obj["name"] %>
<% end %>
<%= f.submit %>
<% end %>
This is definitely not the right approach, as the form will not accept an empty array as a parameter. I'm not sure where else to go with this, any pointers in the right direction would be great. Thanks.
Thanks to MrYoshiji for pointing me in the right direction, this is what ended up working
objects_controller.rb
def
#possible_objects = <api call to get objs>
end
def create
params[:objects].each do |object|
new_obj = Object_Model.new( <params> )
new_obj.save
if !new_obj.save
redirect_to <path>, alert: new_obj.errors.full_messages and return
end
end
redirect_to <path>, notice: 'Successfully created.'
end
objects/new.html.erb
<%= form_tag objects_path(method: :post) do %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag 'objects[]', api_obj %>
<%= possible_object["name"] %>
<% end %>
<%= submit_tag 'Create'%>
<% end %>
Can you try the following?
# view
<% form_tag my_objects_path(method: :post) do |f| %>
<% #possible_objects.each do |api_obj| %>
<%= check_box_tag 'objects[names][]', api_obj["name"] %>
<%= api_obj["name"] %>
<% end %>
<%= f.submit %>
<% end %>
# controller
def create
params[:objects][:names].each do |obj_name|
YourModelForObject.create(name: obj_name)
end
end
See this comment on the documentation of check_box_tag: http://apidock.com/rails/ActionView/Helpers/FormTagHelper/check_box_tag#64-Pass-id-collections-with-check-box-tags

Rails validating with two models

Ok, bit confused on how to solve this issue.
I have one form and two models. Here is my form:
<% if #booking.errors.any? %>
<% #booking.errors.full_messages.each do |msg| %>
<p class="error"><%= msg %></p>
<% end %>
<% end %>
<% if #guest.errors.any? %>
<% #guest.errors.full_messages.each do |msg| %>
<p class="error"><%= msg %></p>
<% end %>
<% end %>
<%= form_for :booking, url: bookings_path do |f| %>
<%= label_tag :email, "Guest's Email Address" %>
<%= text_field_tag :email %>
<%= f.label :nights, "Nights" %>
<%= f.text_field :nights %>
<%= f.label :nights, "People" %>
<%= f.text_field :people %>
<%= f.label :nights, "Arrival Date" %>
<%= f.text_field :arrival %>
<% end %>
As you can see, the email field isn't part of the form builder. The email address will be used to create a new Guest record if the email doesn't already exist. Once I have the ID of the guest then the booking record can be made also.
Here is the code for create action in my BookingController - where the form is submitted to...
...
def create
accommodation = current_user.accommodation
guest = Guest.find_or_create_by(:email => params[:email])
#booking = accommodation.bookings.new(post_params.merge(:guest_id => guest.id))
if #booking.save
flash[:success] = 'The booking has been added successfully.'
redirect_to :controller => 'bookings', :action => 'index'
else
render 'new'
end
end
...
I do realise this question isn't new but I can't find a good solution anywhere to my problem - I want to be able to set the form up properly (if necessary) and validate all fields using the two models. Then I need to display the error messages. At the moment, my email is ignored during validation and I'm not sure what to do next.
Any help much appreciated.
It seems to me that the easiest way to is to validate the email in the controller itself and add any validation error to the booking variable. Something like this:
def create
accommodation = current_user.accommodation
guest = Guest.find_or_create_by(:email => params[:email])
#booking = accommodation.bookings.new(post_params.merge(:guest_id => guest.id))
if #booking.save
flash[:success] = 'The booking has been added successfully.'
redirect_to :controller => 'bookings', :action => 'index'
else
<% if params[:email].blank> %>
#booking.errors.add(:email, "can't be blank.")
<% end %>
#You can do the same thing for whatever other validation errors you have
render 'new'
end
end
Note: I did not test the code
This is probably not the best way possible but it gets the job done and is easy. You could use accept_nested_attributes_for but it seems to me a little bit unnecessary considering that you are only validating an email. Nevertheless, if you want to do it the cleanest way, stick with accept_nested_attributes_for.
EDIT
Actually, your code is the right track. You just made a syntax error. The real reason your guests errors are not being shown is that you used a local variable instead of a instance variable. Try this:
#guest = Guest.find_or_create_by(:email => params[:email])
Your error messages should be displayed with the code you already have
<% if #guest.errors.any? %>
<% #guest.errors.full_messages.each do |msg| %>
<p class="error"><%= msg %></p>
<% end %>
<% end %>
EDIT 2
In order to avoid a booking instance from beings saved in case the guest email is invalid you can do something like this:
if !#guest.errors.any? && #booking.save
flash[:success] = 'The booking has been added successfully.'
redirect_to :controller => 'bookings', :action => 'index'
else
Therefore, if the guest has any errors, the if statement will terminate before the #booking.save statement is executed.
You can try to do a transaction. If one of them is invalid, rails will do a rollback and you can render the errors.
Follow this question code, see if it helps.

checkboxtag in forms

Im looking for the following thing: an array of all users (only 6 in this case) with a checkbox in front of their name, resulting in a list of selectable players for the game.
Current code:
<%= form_for #game, url: games_path, :method => "post" do |f| %>
<%= f.label :name %>
<%= f.text_field :name, :value => "#{current_user.name}\'s Game" %>
<%= f.fields_for :participants do |ff| %>
<%= ff.label :user_id %>
<%= ff.text_field :user_id %>
<%= ff.check_box :user_id %>
<% end %>
<%= f.submit "Create Game", class: "btn btn-primary" %>
<% end %>
I'm now having 3.times { #game.participants.build } in my controller which effectively gives me 3 textfields in which i can fill in the participant id in order to make a record in the table participants (which is linked to games).
I've been looking around for 1.5h now and i cant seem to find a proper answer. What i need is a syntax that gives me a list of all current users (say #users) with a checkbox attached to it. When I click the checkbox it should add its id to the parameters and i should be able to create a new game with the linked participant id's. However I'm getting some problems with the ID's attached to the check_box which always seems to be 1. I've read some stuff about checkboxes being a pain with hashes, but I have no other solution atm.
I tried:
<% #users.each do |i| %>
<%= check_box_tag "alternate_numbers[#{i}]" %> <%= i.name %><br />
<% end %>
But i see no way to get that fixed up part of the form itself.
GamesController code (edit):
def new
#users = User.paginate(page: params[:page])
#games = current_user.games
#game = Game.new
3.times { #game.participants.build }
end
def create
#game = Game.new(params[:game])
#newround = #game.rounds.new
#newround.storyFragment = "New story!"
if #game.save && #newround.save
flash[:success] = "Created!"
redirect_to game_path(#game.id)
else
redirect_to root_url
end
end
It's very vague to describe since im not exactly sure how to accomplish this.
In short: the check_box should contain the value of the user_id in the loop. I'm now filling in a manual ID with the text_field helper but i'd like to have the checkbox linked to the username that is right next to it in the view.
Any guidelines/solutions/tips?
Thx
Okay, so you're making a form for a new Game. You now have to feed that new Game, along with some Participants to your view.
def new
#game = Game.new
#participants = User.all # or the users you want
end
Now use those in your view. You were on the right track. Depending on how your create action works:
<% #participants.each do |p| %>
<%= check_box_tag "participants[#{p.id}]" %> <%= p.name %>
<% end %>
I think what you were missing was the documentation for check_box_tag. The input attribute name is the argument.
You also seem to have a lot of logic in your controllers. Remember to keep the logic in the models, and only use the controllers to give the right objects to your views, and taking them for saving, for example. As the saying goes, "fat model, skinny controller".

simple_form_for not showing fields

I have a model (Project), in the 'new' action I have this code
<h1>Create new project</h1>
<% simple_form_for #project, :url => create_project_path do |project_form| %>
<%= project_form.error_messages %>
<ul>
<% project_form.input :name %>
<% project_form.input :subdomain %>
<% project_form.input :allow_email_report_client %>
<% project_form.input :allow_email_post_client %>
<% project_form.submit %>
</ul>
<% end%>
the controller code is simple
I have a load_and_authorize_resource (using CanCan) and the action code is this
def new
#project = Project.new
end
now, I do get a tag rendered but no fields inside this form
I have the exact same thing for Account model and there all of the fields are shown with no problem.
does anyone have an idea what is the problem?
Ah! You're missing the = signs in your fields:
<%= project_form.input :name %>
<%= project_form.input :subdomain %>
....
Without the =, the code within the <% %> will run, but there will be no output.
Also, good to know: in Rails 2.x you do not need the = for the form_for (Rails 3, however does need the =)
Make sure your new action looks like this:
def new
#project = Project.new
end

Resources