I have the following situation. A contact has_many lead_profiles. I have a new lead_profile form, where the user enters information about the lead_profile and contact. However, I am using a jquery autofill plugin, in that if the contact already exists, the user can select it and then a hidden input id attribute is created to indicate that it's no longer a new contact:
# controller
def new
#lead_profile = LeadProfile.new
#contact = #lead_profile.build_contact name: "Donato"
end
# view
<%= form_for #lead_profile do |f| %>
<%= f.fields_for :contact do |builder| %>
<%= builder.text_field :name %>
<%= builder.text_field :address %>
<% end %>
<% end %>
# javascript
$('#lead_profile_contact_attributes_address').bind('typeahead:selected', function(obj, datum, name) {
var $parent = $("#lead_profile_contact_attributes_address").closest(".form-group");
$parent.after('<input name="lead_profile[contact_attributes][id]" type="hidden" value="' + contact.id + '">')
});
Now when the form is submit to create, I need to determine if the contact exists or not. Note that if I didn't do this, and sent an id attribute for contact back to server, Rails raises an exception "Couldn't find Contact with ID=10 for LeadProfile with ID=". So I must override the contact_attributes= call:
def contact_attributes=(params)
if params[:id].present?
self.contact = Contact.find(params[:id])
else
self.contact = Contact.new params
end
end
Above, I determine if the contact is a new contact or an existing one (which would be the case if the user populates it from javascript autocomplete).
All seems to work fine. If the contact is new, a new contact is created in database with attributes from params hash. If contact is existing, the existing contact is updated with attributes from form. And in both cases, the lead_profiles record is associated with that contact.
But now a problem occurs when I want to update the lead_profile. I use the same form for update as I did for new:
<%= form_for #lead_profile do |f| %>
<%= f.fields_for :contact do |builder| %>
<%= builder.text_field :name %>
<%= builder.text_field :address %>
<% end %>
<% end %>
If I update the contact info in the lead_profile form, the contact is not updated in the database, because of this override that I needed:
def contact_attributes=(params)
if params[:id].present?
self.contact = Contact.find(params[:id])
else
self.contact = Contact.new params
end
end
If I remove that, then the contact would be updated, however then I would get the other issue when creating a new contact:
Couldn't find Contact with ID=10 for LeadProfile with ID=
I really want to save this two models together in a single form, and Rails shouldn't restrict me on that. What can I do at this point?
Ok, so I have one solution working. Remember that when you do an update on a model, it automatically does an update on associations as soon as you invoke save on the model. By default, it will update its association attributes based on the input in the fields_for nested form. However, as soon as you override that default and invoke find, then obviously nothing will be updated on the association:
def contact_attributes=(params)
if params[:id].present?
self.contact = Contact.find(params[:id])
else
self.contact = Contact.new params
end
end
The solution is to somehow distinguish when you are updating a model and when you are dynamically building an existing model association through the form via autocomplete. I used a virtual attribute for that purpose. If the field is being dynamicalyl generated by autocomplete, this is not a rails default and you have to handle it yourself:
model Contact < ActiveRecord::Base
attr_accessor :autocompleted
end
model LeadProfile < ActiveRecord::Base
def contact_attributes=(params)
# this contact model was autocompleted via javascript
if params[:id].present? && params[:autocompleted].present?
self.contact = Contact.find(params[:id])
else
# self.contact = Contact.new params
super
end
end
In all other cases, call super and let Rails do its default behavior.
Just a guess try adding the contact instance in your fields_for call i.e.
<%= f.fields_for :contact,#contact do |builder| %>
<%= builder.text_field :name %>
<%= builder.text_field :address %>
<% end %>
Let me know if that works. I've often had random issues with nested forms and sometimes explicitly setting the target object of the nested form solves the problem
Related
I'm working on a form object that involves updating a User record with it's many associated models like Address and Phone via nested attributes.
Both Address and Phone models have a number attribute, so I name each HTML input and form object attribute differently for each one.
<%= simple_form_for #user_form, method: :patch do |f| %>
<%= f.input :number %>
<%= f.input :phone %>
<% end %>
controller:
def edit
#user_form = UserForm.new(#user)
end
def update
#user_form = UserForm.new(#user, update_params)
if #customer_update.save
redirect_to :users_path
else
render :edit
end
end
def update_params
params.require(:user_form).permit(:number, :phone)
end
Simplified form object:
class UserForm
include ActiveModel::Model
include ActiveModel::Validations::Callbacks
attr_accessor :number
:phone
attr_reader :user
validate :validate_children
def initialize(user, update_params = nil)
#user = user
super(update_params)
end
def save
user.assign_attributes(user_params)
return false if invalid?
user.save
end
def user_params
{}.tap do |p|
p[:address_attributes] = {number: number}
p[:phone_attributes] = {number: phone}
end.compact
end
def validate_children
promote_errors(user.associated_model.errors) if associated_model.invalid?
end
def promote_errors(child_errors)
child_errors.each do |attribute, message|
errors.add(attribute, message)
end
end
end
I delegate validation to the actual Address and Phone models where there is a
validates :number, presence: true
so if validations on either models kick in, the errors are promoted up to the form object and displayed on the invalid input field.
#user_form.errors.keys
=> [:"phone.number", :number]
#user_form.errors.full_messages
=> ["Number can't be blank"]
The problem is that if for example the :phone field is left blank when the form is submited, because the actual attribute that is invalid is the Phone number, the error is displayed in the :number input field in my form, or in other words the Phone number error is rendered in the Address number field because the form input is called the same (f.input :number).
Is there a way to change that or promote the errors differently so this doesn't happen? The code above is obviously dumbed down, let me know if specifics are needed.
That seems like a lot of unnecessary complexity.
The way you would typically set this up in Rails is to use fields_for (and the SF wrapper simple_fields_for):
<%= form_with(model: #user) do |form| %>
<%= form.fields_for(:phone) do |phone_fields| %>
<pre><%= phone_fields.object.errors.full_messages.inspect %></pre>
<% end %>
<%= form.fields_for(:number) do |number_fields| %>
<pre><%= number_fields.object.errors.full_messages.inspect %></pre>
<% end %>
<% end %>
These methods yield a form builder and calling object gets the model instance wrapped by the form builder. This makes it easy to either display the errors together or per attribute and get it right - this is especially true if you're dealing with a one to many or many to many instead of one to one.
I am continuing my project on the tmdb api and in the process of saving the result of my query. I created the method add_tmdb to handle this. As for my view, this is what I have:
= form_tag :action => 'add_tmdb' do
... stuff here
%td= check_box_tag "movie_id[]", movie.id
= submit_tag 'Add selected movie'
My controller is below:
def add_tmdb
movie = {params[:movie_id] => :id}
Movie.create!(movie)
flash[:notice] = "#{params[:movie_id]} was successfully created."
redirect_to movies_path
end
When I submit the record, I get an active record error. This is what I receive:
unknown attribute '["7555"]' for Movie.
Where do I need to make the change?
Thanks!
The value of params[:movie_id] is a single element array. You're using that value as the key of your hash:
{params[:movie_id] => :id}
If you want to specify a key of :id with a value of params[:movei_id], then your hash is backwards. You need
{ id: params[:movie_id] }
You may be accessing an API, but I still think your code needs to be structured better:
#config/routes.rb
resources :movies
#app/controllers/movies_controller.rb
class MoviesController < ApplicationController
def new
#movie = Movie.new
#movies = [[get data for Movies -- from API?]]
end
def create
#movie = Movie.new movie_params
#movie.save
end
private
def movie_params
params.require(:movie).permit(:id, :title, :rating, :release_date)
end
end
Then your form is as follows:
#app/views/movies/new.html.erb
<%= form_for #movie do |f| %>
<%= f.text_field :title %>
<%= f.text_field :rating %>
<%= f.collection_check_boxes :id, #movies, :id, :name %>
<%= f.submit %>
<% end %>
Currently, your flow is not conventional; it doesn't build and object properly and doesn't protect against mass assignment.
Everything in Rails/Ruby is object orientated, meaning that when you "create" a new record, you're building a new object. Might sound trivial, but it's the core of the entire Rails framework.
Your comment of "How do I proceed in adding the title, rating and release date of the movie" demonstrates the ramifications of not adhering to it.
In short, you need new,create actions, both referencing a new Movie object, which you can then populate the with the params sent from your views.
API
The only exception you should make to the above would be if your flow is dependent on API callbacks, like through a billing system or something.
If you want to retain your current flow (IE call add_tmdb), you'll want to make sure you're populating the attributes of your Movie object as follows:
#app/controllers/movies_controller.rb
class MoviesController < ApplicationController
def add_tmdb
#movie = Movie.new
#movie.id = params[:movie_id]
#movie.save
#or....
#movie = Movie.new({id: params[:movie_id]})
#movie.save
end
end
I have a form where users enter profile info which is saved in the User model. I need to collect another piece of information from users at a later stage (on a different page). How do I collect this info and append it to the existing user record?
Here is my form code with the one additional input I need from the user. When I hit submit, it runs without error but the new field is not saved in my model. I ran the migration to add the field to the model.
<%= form_for #user, html: { method: :put }) do |f| %>
<div class="form-group">
<%= f.label :what_is_your_favorite_color %>
<%= f.text_field :color, class:"form-control" %>
</div>
<div class="form-group">
<%= f.submit "Submit", class:"btn btn-primary" %>
</div>
<% end %>
My controller update method is currently blank .. can you tell me what the update method should look like? The user is logged in so I need to find that user record (probably by id column?) and write or update the new input into the model.
def update
??
end
You first need to fetch your record, the pass it the params hash, and Rails will do the rest.
def update
#record = Record.find(params[:id])
if #record.update(record_params)
redirect_to #record
else
render :edit
end
end
If you are using Rails 4, you need to account for strong_parameters. So add the new attribute to the permitted attributes.
def record_params
params.require(:record).permit(:color, :and, :other, :attributes, :go, :here)
end
The above code assumes that the record id will be in the params hash, or in other words, you are using RESTful routes. If you are not, you may want to pass in the id from the session (if this is, as you say, a user record, and you are using Devise).
def update
#record = Record.find(current_user)
# ... the rest should be the same
end
I am making one of my first rails apps from scratch and cannot figure out what is going on. Whenever I try to create a new instance, it saves multiple instances of the item. I am trying to make tasks, and each time I create a new instance it saves 7 times, and then gives me an error page saying no data received. I tried adding a uniqueness validator for the model, but that would save the first instance then return me to root path. This is what I have:
Task Controller:
class TasksController < ApplicationController
def index
#tasks = Task.all
end
def create
#task = Task.new(task_params)
if #task.save
redirect_to 'tasks'
else
redirect_to root_path
end
end
private
def task_params
params.require(:task).permit(:title, :category, :difficulty)
end
end
Here is the form I am using, perhaps that is the problem?
<div id='task_form'>
<%= form_for :task do |f| %>
<%= f.label :title %>
<%= f.text_field :title%>
<%= f.label :category %>
<%= f.text_field :category%>
<%= f.label :difficulty %>
<%= f.text_field :difficulty%>
<%= f.submit %>
<% end %>
</div>
Thanks if anyone knows what this is or has experienced it before
Do this:
#app/controllers/tasks_controller.rb
def new
#task = Task.new
end
#app/views/tasks/new.html.erb
<%= form_for #task do |f| %>
--
Problem
I frankly don't know why you're getting multiple submits, but I can see one of the issues you have is you're using a symbol in place of an instance variable.
This wouldn't normally be an issue, but as you're experiencing these problems, I would certainly use an instance variable in the form. (if only to test).
This does several important things:
Using an #instance var creates an ActiveRecord object for your form
The use of an ActiveRecord object gives rails a definite structure for the object
By saving the object, Rails is able to populate the appropriate attributes etc
I would certainly try the above code, but there may be other issues at work here if your form is submitting 7 times.
I'm using Rails 2.3.2, and trying to get a nested object form to work properly. I've narrowed my problem to the issue that Rails is not setting my nested form elements with the *_attributes required to initiate the accepts_nested_attributes_for processing.
My model code is:
class Person < Party
has_one :name, :class_name => "PersonName"
accepts_nested_attributes_for :name, :allow_destroy => true
end
class PersonName < ActiveRecord::Base
belongs_to :person
end
My view code looks like this (I'm using HAML):
%h3 New customer
= error_messages_for :person, :person_name, :name, :country
- form_for :person, :url => collection_url, :html => {:class => 'MainForm'} do |person_form|
- #person.build_name unless #person.name
- person_form.fields_for :name do |name_form|
= name_form.label :given_name, "First Name:"
= name_form.text_field :given_name
= name_form.label :family_name, "Last Name:"
= name_form.text_field :family_name
= hidden_field_tag :inviter_id, params[:inviter_id]
= hidden_field_tag :inviter_code, params[:inviter_code]
%p= submit_tag "Create"
= link_to 'Back', collection_url
Instead of params being:
{"person"=>{"name_attributes"=>{"given_name"=>"Fred", "family_name"=>"Flintstone"}}, ...}
I get:
{"person"=>{"name"=>{"given_name"=>"Fred", "family_name"=>"Flintstone"}}, ...}
As a result, I get a TypeMismatch exception. I've followed the documentation from Ryan Daigle. I've also followed the advice from this blog and the complex-forms-example.
Using Firebug, I went through my form and adjusted the name attribute of the input tags from name to name_attributes. This produced the params with name_attributes, and the create worked fine.
I'm stuck as I cannot figure out why my form is not producing the *_attributes form of the name.
Another thing I tried is I got the complex_form_example working in my environment. I've gone through every inch of the controller, models and views and compared it to my code. I cannot find what is different. I know this is something small, and would appreciate any help!
Thanks!
Post backs do not get routed to the right place
def new
#person = Person.new
end
<% form_for #person do |f| %>
<% f.fields_for :name_attributes do |p| %>
...
<% end %>
<% end %>
Post backs get routed to the right place
def new
#person = Person.new
#person.name = PersonName.new # << this allows fields_for :relation vs :relation_attributes
end
<% form_for #person do |f| %>
<% f.fields_for :name do |p| %>
...
<% end %>
<% end %>
No need to #person.name again in #create
Try to use an actual object for form_for:
form_for :person => form_for #person
I have just been struggling for about an hour with exactly the same problem!
Follow nowk's pattern for the new method in the controller, then put this in your view
<% form.fields_for :name, #person.name do |name_form| %>
<% end %>
Good luck if you try it, that's what worked for me.
Not sure why this isn't working for, but as a workaround, you could just use params[:name] in your controller#create method to update the person record.
person = Person.new(params[:person])
person.name << PersonName.new(params[:name])
Unfortunately, I still have not been able to figure out why this form wouldn't work with nested object forms. I stripped it down to the simplest data, started over using the complex-form-example as a start. I ended using the active_presenter to gather x-object data from the form. I'll revisit nested object forms sometime in the form. Thanks for your help.
Thought I would share my solution as it's slightly different - in my case I want the nested attributes to be dynamic.
In new action:
case params[:type]
when "clubber"
#account = resource.send "build_#{params[:type]}"
when "promoter"
#account = resource.send "build_#{params[:type]}"
when "company"
#account = resource.send "build_#{params[:type]}"
when "venue_owner"
flash[:notice] = 'We ask that venue owners register via the web. Thanks.'
redirect_to root_path and return
end
In my view:
= f.fields_for #account.class.name.downcase+'_attributes' do |form|
Pretty ghetto, but it works.