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 %>
Related
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
I am having a tough time figuring out how to make a form_object that creates multiple associated objects for a has_many association with the virtus gem.
Below is a contrived example where a form object might be overkill, but it does show the issue I am having:
Lets say there is a user_form object that creates a user record, and then a couple associated user_email records. Here are the models:
# models/user.rb
class User < ApplicationRecord
has_many :user_emails
end
# models/user_email.rb
class UserEmail < ApplicationRecord
belongs_to :user
end
I proceed to create a a form object to represent the user form:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
attribute :emails, Array[EmailForm]
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)
# emails.each do |email_form|
# UserEmail.create(user: user, email: email_form.email_text)
# end
end
end
One will notice in the UserForm class that I have the attribute :emails, Array[EmailForm]. This is an attempt to validate and capture the data that will be persisted for the associated user_email records. Here is the Embedded Value form for a user_email record:
# app/forms/email_form.rb
# Note: this form is an "Embedded Value" Form Utilized in user_form.rb
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
Now I will go ahead and show the users_controller which sets up the user_form.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
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 #user, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails: [:email_text]})
end
end
The new.html.erb:
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
And the _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>
<% unique_index = 0 %>
<% f.object.emails.each do |email| %>
<%= label_tag "user_form[emails][#{unique_index}][email_text]","Email" %>
<%= text_field_tag "user_form[emails][#{unique_index}][email_text]" %>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Note: If there is an easier, more conventional way to display the inputs for the user_emails in this form object: let me know. I could not get fields_for to work. As shown above: I had to write out the name attributes by hand.
The good news is that the form does render:
The html of the form looks ok to me:
When the above input is submitted: Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "emails"=>{"0"=>{"email_text"=>"foofoo"}, "1"=>{"email_text"=>"bazzbazz"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
The params hash looks ok to me.
In the logs I get two deprecation warnings which makes me think that virtus might be outdated and thus no longer a working solution for form objects in rails:
DEPRECATION WARNING: Method to_hash is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
DEPRECATION WARNING: Method to_a is deprecated and will be removed in Rails 5.1, as ActionController::Parameters no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.2/classes/ActionController/Parameters.html (called from new at (pry):1)
NoMethodError: Expected ["0", "foofoo"} permitted: true>] to respond to #to_hash
from /Users/neillocal/.rvm/gems/ruby-2.3.1/gems/virtus-1.0.5/lib/virtus/attribute_set.rb:196:in `coerce'
And then the whole thing errors out with the following message:
Expected ["0", <ActionController::Parameters {"email_text"=>"foofoo"} permitted: true>] to respond to #to_hash
I feel like I am either close and am missing something small in order for it to work, or I am realizing that virtus is outdated and no longer usable (via the deprecation warnings).
Resources I looked at:
this article.
this video
I did attempt to get the same form to work but with the reform-rails gem. I ran into an issue there too. That question is posted here.
Thanks in advance!
I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. That way you don't have to customize the form fields.
Complete Answer:
Models:
#app/modeles/user.rb
class User < ApplicationRecord
has_many :user_emails
end
#app/modeles/user_email.rb
class UserEmail < ApplicationRecord
# contains the attribute: #email
belongs_to :user
end
Form Objects:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
validates :name, presence: true
validate :all_emails_valid
attr_accessor :emails
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|
UserEmail.new(email: email.email_text)
end
user.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
# "Embedded Value" Form Object. Utilized within the user_form object.
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
Controller:
# app/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
Views:
#app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
#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 %>
You have an issue because you haven't whitelisted any attributes under :emails. This is confusing, but this wonderful tip from Pat Shaughnessy should help set you straight.
This is what you're looking for, though:
params.require(:user_form).permit(:name, { emails: [:email_text, :id] })
Note the id attribute: it's important for updating the records. You'll need to be sure you account for that case in your form objects.
If all this form object malarkey with Virtus gets to be too much, consider Reform. It has a similar approach, but its raison d'etre is decoupling forms from models.
You also have an issue with your form… I'm not sure what you were hoping to achieve with the syntax you're using, but if you look at your HTML you'll see that your input names aren't going to pan out. Try something more traditional instead:
<%= f.fields_for :emails do |ff| %>
<%= ff.text_field :email_text %>
<% end %>
With this you'll get names like user_form[emails][][email_text], which Rails will conveniently slice and dice into something like this:
user_form: {
emails: [
{ email_text: '...', id: '...' },
{ ... }
]
}
Which you can whitelist with the above solution.
The problem is that the format of the JSON being passed to UserForm.new() is not what is expected.
The JSON that you are passing to it, in the user_form_params variable, currently has this format:
{
"name":"testform",
"emails":{
"0":{
"email_text":"email1#test.com"
},
"1":{
"email_text":"email2#test.com"
},
"2":{
"email_text":"email3#test.com"
}
}
}
UserForm.new() is actually expecting the data in this format:
{
"name":"testform",
"emails":[
{"email_text":"email1#test.com"},
{"email_text":"email2#test.com"},
{"email_text":"email3#test.com"}
}
}
You need to change the format of the JSON, before passing it to UserForm.new(). If you change your create method to the following, you won't see that error anymore.
def create
emails = []
user_form_params[:emails].each_with_index do |email, i|
emails.push({"email_text": email[1][:email_text]})
end
#user_form = UserForm.new(name: user_form_params[:name], emails: emails)
if #user_form.save
redirect_to #user, notice: 'User was successfully created.'
else
render :new
end
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 am trying to use fields_for and create a nested form, however only one text field shows up, blank. I have 3 crewmember records.
crewmember model:
class Crewmember < ActiveRecord::Base
belongs_to :production
belongs_to :callsheet
validates :firstname, presence: true
validates :email, presence: true
def name
"#{firstname} #{lastname}"
end
end
callsheet model
class Callsheet < ActiveRecord::Base
attr_accessible :crewmembers_params
has_many :castmembers
has_many :crewmembers
accepts_nested_attributes_for :crewmembers
end
callsheets controller
class CallsheetsController < ApplicationController
def index
#callsheets = Callsheet.all
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def show
#callsheet = Callsheet.find(params[:id])
end
def new
#callsheet = Callsheet.new
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def edit
#callsheet = Callsheet.find(params[:id])
end
def create
#callsheet = Callsheet.new(callsheets_params)
#Callsheet.production_id = current_user.default_working_production_id
if #callsheets.save
redirect_to callsheet_path
else
render 'new'
end
end
def update
#callsheet = Callsheet.find(params[:id])
if #callsheet.update(callsheets_params)
redirect_to callsheet_path, :notice => "callsheets successfully updated."
else
render 'edit', :notice => "callsheets not updated."
end
end
def destroy
#callsheet = Callsheet.find(params[:id])
#callsheet.destroy
redirect_to callsheets_path
end
private
def callsheets_params
params.require(:callsheet).permit(:crewmembers_params [:id, :firstname])
end
end
form for new callsheet:
<%= form_for #callsheet do |f| %>
<% if #callsheet.errors.any? %>
<div id="error_explanation" class="alert alert-danger">
<strong>
<%= pluralize(#callsheet.errors.count, "error") %> prohibited
this call sheet from being saved:
</strong>
<ul>
<% #callsheet.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.fields_for :crewmember do |crewmember| %>
<fieldset>
<%= crewmember.label :firstname, "First Name" %><br />
<%= crewmember.text_field :firstname %>
</fieldset>
<% end %>
<% end %>
You don't need attr_accessible (that's only for Rails 3).
You should also rename all your models to snake_case, referencing with CamelCase:
#app/models/call_sheet.rb
class CallSheet < ActiveRecord::Base
has_many :cast_members
has_many :crew_members
accepts_nested_attributes_for :crew_members
end
As is the custom with fields_for, you also need to build the associated objects (if you're creating a new record) (you don't need to do this if editing):
#app/controllers/call_sheets_controller.rb
class CallSheetsController < ApplicationController
before_action :set_departments
def new
#callsheet = Callsheet.new
#callsheet.crew_members.build
end
def edit
#callsheet = Callsheet.find params[:id]
end
def update
#callsheet = Callsheet.find params[:id]
#callsheet.update callsheet_params
end
private
def set_departments
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def callsheet_params
params.require(:callsheet).permit(crew_members_attributes: [:id, :firstname])
end
end
This will allow you to use:
<%= form_for #callsheet do |f| %>
<%= f.fields_for :crew_members do |crewmember| %>
<%= crewmember.label :firstname, "First Name" %><br />
<%= crewmember.text_field :firstname %>
<% end %>
<%= f.submit %>
<% end %>
--
When passing nested attributes through fields_for, you need several components:
The correct association in your parent model
An instantiated version of the associated model (#parent.build_child)
Correct fields_for definition
Passing correct parameters through your controller
I've outlined how to achieve the above, all of which you had incorrect.
You can also declare multiple validations in the same call:
#app/models/crew_member.rb
class CrewMember < ActiveRecord::Base
validates :firstname, :email, presence: true
end
Try changing
<%= f.fields_for :crewmember do |crewmember| %>
into
<%= f.fields_for :crewmember, #callsheet.crewmember || #callsheet.build_crewmember do |crewmember| %>
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)