I have a nested form which captures information for two models, Games and Teams.
My Models:
class Game < ApplicationRecord
has_many :teams
accepts_nested_attributes_for :teams
validates_associated :teams
validates :start_time, presence: true
end
class Team < ApplicationRecord
belongs_to :game
validates :name, presence: true, length: { maximum: 50 }
end
Before saving the records, the validations must be passed. If the save fails, the form should be re-rendered and the validation error messages displayed, as per the controller below. However, the error messages never get displayed.
My GamesController:
class GamesController < ApplicationController
def new
#game = Game.new
#team = #game.teams.build
end
def create
#game = Game.new(game_params)
unless #game.save
render 'new'
return
end
# Some other code that shouldn't run if the save fails, hence the 'return' above
end
end
My form (new.html.erb):
<%= render 'shared/error_messages' %>
<%= form_with model: #game do |f| %>
<%= f.fields_for :teams do |f_teams| %>
<%= f_teams.label :name %>
<%= f_teams.text_field :name, class: 'form-control'%>
<%= f.label :start_time, "Game day" %>
<%= f.date_field :start_time, id: "game_day", class: 'form-control' %>
<%= f.submit "Book now!", class: "btn btn-primary" %>
<% end %>
<% end %>
and finally, the error message partial:
<% if #game.errors.any? %>
<div id="error_explanation">
<div class="alert alertdanger">
The form contains <%= pluralize(#game.errors.count, "error") %>.
</div>
<ul>
<% #game.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
When I deliberately trip up the validations (e.g. by not including the game day) the error message partial doesn't run, presumably because the #game.errors.any? is false.
If I use byebug or if I go throug the rails console, I get the validation errors, e.g. start_time can't be blank.
What am I missing here?
EDIT
Chris's solution below worked for me. However, I wanted my controller to run JS if validations were met and save succeeded. So I went back and removed the suggested local: true and allowed the remote submission to happen. What I did to fix the issue is render html if save didn't succeed:
unless #game.save
respond_to do |format|
format.html { render 'new' }
end
return
end
This didn't work out of the box because turbolinks interferes. I therefore ended up adding gem 'turbolinks_render' to my Gemfile and voila everthing works great now.
Huge shoutout to Joel (https://joelc.io/ajax-ruby-on-rails-forms) for the walkthrough.
In your new.html.erb file, try
<%= form_with(model: #game, local: true) do |f| %>
Some good info for using the form_with tag here:
https://medium.com/#tinchorb/form-with-building-html-forms-in-rails-5-1-f30bd60ef52d
Related
I'm trying to show error messages when creating a new instance of a model in my Rails app when a field doesn't pass validation. For some reason, the errors never actually show up on the website next to the fields like they're supposed to. However, the errors appear in the 'Preview' section of the Network tab of Chrome DevTools. So the errors are generating properly. In the terminal it says that new.html.erb is rendered but I don't think it actually does? Any help would be greatly appreciated - I haven't found much about this online. I'm using Tailwind CSS for styling the front end if that's helpful.
Here's my code:
occasion.rb
class Occasion < ApplicationRecord
belongs_to :user
has_many :registries
has_many :charities, :through => :registries
validates :occasion_name, presence: true
validates :occasion_date, presence: true
validates :url, presence: true, format: { without: /\s/ }, uniqueness: true
validates_uniqueness_of :user_id
end
occasions_controller.rb
class OccasionsController < ApplicationController
load_and_authorize_resource only: [:new, :create, :edit, :update, :destroy]
def index
#occasions = Occasion.all
end
def show
#occasion = Occasion.where(url: params[:url]).first
end
def new
#occasion = Occasion.new
end
def create
#occasion = Occasion.new(occasion_params)
#occasion.user = current_user
if #occasion.save
respond_to do |format|
format.html { redirect_to new_registry_path }
format.js { render :js => "window.location='#{ new_registry_path }'" }
end
else
render :new
end
end
def edit
#occasion = Occasion.find(params[:id])
end
def update
#occasion = Occasion.find(params[:id])
if #occasion.update(occasion_params)
redirect_to #occasion
return
else
render :edit
end
end
def destroy
#occasion = Occasion.find(params[:id])
#occasion.destroy
redirect_to occasions_path
end
private
def occasion_params
params.require(:occasion).permit(:user_id, :occasion_name, :occasion_date, :url)
end
# user authentication is not required for show
skip_before_action :authenticate_user!, :only => [:show]
end
new.html.erb
<%= form_with model: #occasion do |form| %>
<div class="text-center">
<%= form.label :occasion_name, "Occasion Name", class: "text-red-400 font-semibold px-8" %><br>
<%= form.text_field :occasion_name, class: "rounded w-2/5" %>
<% #occasion.errors.full_messages_for(:occasion_name).each do |message| %>
<div><%= message %></div>
<% end %>
</div>
<div class="text-center py-2">
<%= form.label :occasion_date, "Occasion Date", class: "text-red-400 font-semibold px-8" %><br>
<%= form.date_field :occasion_date, type: "date", class: "rounded" %>
<% #occasion.errors.full_messages_for(:occasion_date).each do |message| %>
<div><%= message %></div>
<% end %>
</div>
<div class="text-center py-2">
<%= form.label :url, 'URL', class: "text-red-400 font-semibold px-8" %><br>
<%= form.text_field :url, class: "rounded" %>
<% #occasion.errors.full_messages_for(:url).each do |message| %>
<div><%= message %></div>
<% end %>
<em><div class="text-sm">domainname.com/yourURLhere</div></em>
</div>
<div class="text-center py-2">
<%= form.submit occasion.persisted? ? 'Update' : 'Save', class: "rounded-full bg-red-400 text-white px-3" %>
</div>
<% end %>
From the provided information, it looks like the form gets submitted as an AJAX request. Since you're not passing local: false to the form_with call, there must be a configuration set to use AJAX form submissions by default.
From the docs,
:local - By default form submits via typical HTTP requests. Enable remote and unobtrusive XHRs submits with local: false. Remote forms may be enabled by default by setting config.action_view.form_with_generates_remote_forms = true.
Pass local: true to submit the request via a normal HTTP request.
<%= form_with model: #occasion, local: true do |form| %>
<%#= ... %>
<% end %>
The example code below is a contrived example of an attempt at a form object where it is probably overkill to utilize a form object. Nonetheless: it shows the issue I am having:
I have two models: a User and an Email:
# app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
# app/models/user.rb
class Email < ApplicationRecord
belongs_to :user
end
I want to create a form object which creates a user record, and then creates three associated email records.
Here are my form object classes:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :email_forms
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
user = User.create(name: name)
email_forms.each do |email|
Email.create(user: user, email_text: email.email_text)
end
end
end
# app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
# DON'T THINK I WOULD PERSIST DATA HERE
# INSTEAD DO IT IN THE user_form
end
end
Notice: the validations on the form objects. A user_form is considered to be invalid if it's name attribute is blank, or if the email_text attribute is left blank for any of the email_form objects inside it's email_forms array.
For brevity: I will just be going through the new and create action of utilizing the user_form:
# app/controllers/user_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.email_forms = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {email_forms: [:_destroy, :id, :email_text, :user_id]})
end
end
Lastly: the form itself:
# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
# MESSY, but couldn't think of a better way to do this...
<% unique_index = 0 %>
<% user_form.email_forms.each do |email_form| %>
<div class="field">
<%= label_tag "user_form[email_forms][#{unique_index}][email_text]", "Email Text" %>
<%= text_field_tag "user_form[email_forms][#{unique_index}][email_text]" %>
</div>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The form does render:
And here is the form's html:
I go to submit the form. Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "email_forms"=>{"0"=>{"email_text"=>"test_email_1"}, "1"=>{"email_text"=>"test_email_2"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
What should happen is the form should be re-rendered and nothing persisted because the form_object is invalid: All three associated emails must NOT be blank. However: the form_object thinks it is valid, and it blows up in the persist! method on the UserForm. It highlights the Email.create(user: user, email_text: email.email_text) line and says:
undefined method `email_text' for ["0", {"email_text"=>"test_email_1"}]:Array
Clearly there are a couple things going on: The nested validations appear to not be working, and I am having trouble rebuilding each of the emails from the params hash.
Resources I have already examined:
This Article seemed promising but I was having trouble getting it to work.
I have attempted an implementation with the virtus gem and the reform-rails gem. I have pending questions posted for both of those implementations as well: virtus attempt here and then reform-rails attempt here.
I have attempted plugging in accepts_nested_attributes, but was having trouble figuring out how to utilize that with a form object, as well as a nested form object (like in this code example). Part of the issue was that has_many and accepts_nested_attributes_for do not appear to be included in ActiveModel::Model.
Any guidance on getting this form object to do what is expected would be very much appreciated! Thanks!
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I have a nested relationship where dashboard has many rewards, and I am trying to add a fields_for to the page in order to edit the rewards. Unfortunately, it doesn't seem to be working and I don't know why.
Here's what I have.
Dashboard model:
class Dashboard < ActiveRecord::Base
belongs_to :manager
has_many :rewards
accepts_nested_attributes_for :rewards, allow_destroy: true
end
Rewards model:
class Reward < ActiveRecord::Base
belongs_to :dashboard
end
Dashboard controller:
class DashboardsController < ApplicationController
before_action :authenticate_manager!
# Requires user to be signed in
def index
#dashboards = Dashboard.all
end
def new
#dashboard = Dashboard.new
end
def edit
#dashboard = Dashboard.find(params[:id])
end
def create
#dashboard = Dashboard.new(dashboard_params)
#dashboard.save
if #dashboard.save
redirect_to dashboard_path(#dashboard)
else
render :action => new
end
end
def update
#dashboard = Dashboard.find(params[:id])
if #dashboard.update(dashboard_params)
redirect_to :action => :show
else
render 'edit'
end
end
def show
#dashboard = Dashboard.find(params[:id])
end
def destroy
#dashboard = Dashboard.find_by_id(params[:id])
if #dashboard.destroy
redirect_to dashboards_path
end
end
private
def dashboard_params
args = params.require(:dashboard).permit(:title, :description, :rewards, {rewards_attributes: [ :id, :title, :referralAmount, :dashboardid, :selected, :_destroy] } )
args
end
end
Form in dashboards view:
<%= form_for :dashboard, url: dashboard_path(#dashboard), method: :patch do |f| %>
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_field :description %>
</p>
<%= f.fields_for :rewards do |reward| %>
<%= reward.label :title %><br>
<%= reward.text_field :title %>
<%= reward.check_box :_destroy %>
<%= reward.label :_destroy, "Remove reward" %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I went ahead and manually added rewards to the database through the rails console and it worked beautifully, but they are not showing up on the page. They will show up if I iterate through them like so
<% if #dashboard.rewards.any? %>
<ul>
<% #dashboard.rewards.each do |reward| %>
<li><%= reward.title %></li>
<li><%= reward.referralAmount %></li>
<% end %>
</ul>
<% else %>
<p>no rewards</p>
<% end %>
However the fields_for does not display the rewards or their content and resultingly allow one to edit them.
Let me know if you need further information/code.
Try to modify your:
View:
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for #dashboard, url: dashboard_path(#dashboard) do |f| %>
........
<% end %>
Controller (has_many relationship):
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
private
def dashboard_params
params.require(:dashboard).permit(:title, :description,
rewards_attributes: [
:id,
:title,
:referralAmount,
:dashboardid,
:selected,
:_destroy
])
end
You don't have to set the method: patch if form.
Once you got in edit page, Rails will use the update action in controller when form submission.
To check it, run rake routes,
you will see somsthing like this:
PATCH /dashboards/:id(.:format) dashboards#update
PUT /dashboards/:id(.:format) dashboards#update
In controller you need to give build
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
"build" is just create a new object in memory so that the view can take this object and display something, especially for a form.
Hope it helps for you
You should build object before nested form. You can add whatever you want that object.
Try it in controller;
def new
#dashboard = Dashboard.new
3.times do
#dashboard.build_reward
end
end
Try setting an "#rewards" instance variable in your dashboards edit method (where #rewards = #dashboard.rewards). Then replace :rewards with #rewards.
Edit:
I believe my initial answer is inapproriate for your exact question (while it would be helpful on say the page to show a specific dashboard and its rewards). The answers above are on the right track re:
refining your params method per #aldrien.h;
Adding #santosh dadi's suggestion of
#dashboard.rewards.build
(assuming you only want one rewards fields on a form for "new")
Finally though, to avoid making fake information for a new rewards form, adding to the top of your Dashboards model:
accepts_nested_attributes_for :rewards, reject_if: lambda {|attributes| attributes['title'].blank?}
http://guides.rubyonrails.org/form_helpers.html#nested-forms
I have three-tier model:
User has_many Asks has_many Outcomes
On the home page, I would like the user to be able to add an Outcome to their Ask when they mark it complete. I'm trying to use a nested form to display the Outcome description in the Ask form which also updates the done flag and done date.
Like other users/questions here on SO, I cannot get a nested form to display on the screen. I've followed instructions from the other questions, but still the nested field is not displaying. Am wondering if someone can spot the issue in the code below?
Ask Model
class Ask < ActiveRecord::Base
attr_accessible :category, :description, :done, :followed_up,
:helper, :public, :date_done, :date_followed_up, :user_id, :outcomes_attributes
belongs_to :user, counter_cache: true
has_many :outcomes
accepts_nested_attributes_for :outcomes
end
Ask Controller
class AsksController < ApplicationController
def new
#ask = current_user.asks.build(params[:ask])
#ask.outcomes.build
end
def create
#ask = current_user.asks.build(params[:ask])
if #ask.save!
respond_to do |format|
format.html { redirect_to edit_ask_path(#ask) }
format.js
end
else
flash[:error] = "Something is wrong. The Ask was not saved..."
end
end
def edit
#ask = current_user.asks.find(params[:id])
end
def update
#ask = current_user.asks.find(params[:id])
#ask.outcomes.build
#ask.update_attributes(params[:ask])
respond_to do |format|
format.html { redirect_to edit_ask_path(#ask) }
format.js
end
end
end
Home Page Controller (this form is on the home page)
class StaticPagesController < ApplicationController
def home
if signed_in?
#ask = current_user.asks.build(params[:ask])
#ask.outcomes.build
end
end
Form Partial rendered on the home page
<% if current_user.asks.any? %>
<ul id="ask-list-items">
<% current_user.asks.where(done: false).each do |a| %>
<%= form_for(a) do |f| %>
<li><%= a.description %></li>
<%= f.hidden_field :date_done, value: Date.today %>
<%= f.hidden_field :done, :value=>true %>
<%= f.submit "Mark as done", class: "btn btn-small hidden done_btn", id: "a-#{a.id}-done" %>
<%= f.fields_for :outcomes do |builder| %> # << These fields are not showing up
<%= builder.text_area :description, placeholder: "Describe the outcome...", id: "ask-message" %>
<% end %>
<%= f.submit "Save outcome", class: "btn btn-primary" %>
<% end %>
<% end %>
</ul>
<% end %>
When using symbol in form_for and fields_for Rails tries to use an instance variable with he same name, e.g. #outcomes for :outcomes. So try (for existing outcomes):
<% #outcomes = a.outcomes %>
before the line with f.fields_for :outcomes....
And for new outcomes:
<% #outcomes = a.outcomes.build %>
(the last with contribution to the owner of the question)
I'm not sure how to display the error messages for my form when using it in this form_tag scenario. My code below allows me to create 5 products at once on a form but unfortunately only renders the notice that "an error occurred...".
Here is my code:
Product.rb
class Product < ActiveRecord::Base
attr_accessible :price, :name, :purchase_date, :product_store, :in_category
belongs_to :user
belongs_to :store
attr_reader :product_store
validates_inclusion_of :in_category, :in => [true, false]
validates_presence_of :name, :price, :store_id, :user_id
validates_numericality_of :price
def product_store=(id)
self.store_id = id
end
end
Products_controller.rb
class ProductsController < ApplicationController
def new
#products = Array.new(5) { Product.new }
end
def create_multiple
#products = current_user.products.create(params[:products].map { |_k, p| p.merge params[:product] })
if #products.each(&:save)
redirect_to :back, :notice => "Success!"
else
redirect_to :back, :notice => "An error occured, please try again."
end
end
end
Form.html.erb
<%= form_tag create_multiple_products_path, :method => :post do %>
<%= error_messages_for #product %>
# the :purchase_date and :in_category are merged into all 5 Products.
<%= date_select("product", "purchase_date") %>
<%= label_tag :in_category, 'Add to Category?' %>
<%= radio_button("product", :in_category, 1) %>
<%= radio_button("product", :in_category, 0) %>
<% #products.each_with_index do |product, index| %>
<%= fields_for "products[#{index}]", product do |p| %>
<%= render "fields", :f => p %>
<% end %>
<% end %>
<%= submit_tag "Done" %>
<% end %>
Theirs 2 issues. 1. Getting the validations for the fields outside of the fields_for to show .2. And then the ones inside of the fields_for. How could I do this?
Thank you.
I've been trying to do much the same thing, with this:
<% #products.each_with_index do |product, index| %>
<% product.errors.full_messages.each do |value| %>
<li><%= value %></li>
<% end %>
However, this only shows errors for the first product with errors. You submit it, and if there is a subsequent product with errors, you are sent back to that page, and that next product with errors shows its errors, etc.
EDIT: Got it. It has to do with how I was validating. Instead of this:
if #products.all?(&:valid?)
do this:
#products.each(&:valid?) # run the validations
if #products.all? { |t| t.errors.empty? }