Rails nested polymorphic form with javascripting - ruby-on-rails

I want the following on my form:
Degree is polymorphic partial with has_many relation to profile. This partial is called by main form - profile. By pressing 'Add a Qualification' user can add as many degrees. I've been able to attach only one degree to profile which is the last one others are all ignored. I even know why it's happening because link_to is not able to pass profile instance. so I have to create new profile in degrees_controller as you can see in my code here. Can final profile instance pick up all others above when submitting 'Create Profile'.
Kindly any help so that I can have all of the degrees attached with form, I'm stuck on this for last couple of days with all permutations and combinations from SO and google. I'm ready to change code even....any help with this will be appreciated.

I've got this little helper to get me through these cases. Its reusable so you don't ever have to write all this stuff for different cases in the same app.
First the code:
module FormsHelper
def data_for_field(f, subclass, options={})
new_object = f.object.class.new.send(subclass.to_s).new(options[:attributes])
id = new_object.object_id
partial_path = options[:path] || new_object
fields = f.fields_for(subclass, new_object, child_index: id) do |builder|
render(partial_path, f: builder)
end
{id: id, fields: fields.gsub("\n", "")}
end
end
Some JS magic:
$(document).on('click', '.add-fields', function(e){
var time = new Date().getTime();
var regexp = new RegExp($(this).data('id'), 'g');
var content = $(this).data('fields').replace(regexp, time);
$(target).append(content);
e.preventDefault();
});
Now a view call:
<%= form_tag .... do |f|
<div id="list"></div>
<%= link_to 'Add an Organisation', '#', class: 'add-fields', data: data_for_field(f, :degrees, path: "/degrees/fields").merge(target: "#list") %></p>
<% end %>
Now create a partial where the degree fields to use.
/degrees/_fields.html.erb
<%= content_tag :div do %>
<%= f.input :name %>
<% end %>
Now the explanation:
For data_for_field you pass: f (form), :degrees (the plural name of the associated model as a symbol), and any additional options like the path to the partial for the fields. It basically creates a scaffold of the form fields and sets them as a data attribute on the "Add Qualification" link. I've merged in a data-target which has the css ID of where to insert the new fields into the page as well.
When you click the JS event fires and replaces the id in the form fields with a timestamp so for each qualification you'll have a unique key.
I also usually have a little extra JS event for removing the fields as well.
Hope this helps.

This is a polymorphic nested association and can be handled on client side with javascript. So, finally for nested fields I used the plugin Numerous.js. Just follow as the steps given in the qucikstart part of link by downloading the numerous.js file from the Github and saving to assets/javascripts.
In my code,
profile.rb
class Profile < ApplicationRecord
has_many :degrees, :as => :degreeable
accepts_nested_attributes_for :degrees, :reject_if => :all_blank, allow_destroy: true
belongs_to :user, :class_name => 'User', :foreign_key => 'user_id'
end
degree.rb
class Degree < ApplicationRecord
belongs_to :degreeable, polymorphic: true
end
profiles/_form.html.erb
<div id="fields-for-list" class="numerous">
<%= f.fields_for :degrees, Degree.new, :child_index => 'replace_this' do |degree_form| %>
<%= degree_form.select :level, options_for_select(Job::EDUCATION, params[:level]), include_blank: "Select Degree", :class => 'span5' %>
<%= degree_form.text_field :description, placeholder: "Add a new Degree here..."%>
<%= link_to 'x Remove', '#', :class => 'numerous-remove', type: 'button' %>
<% end %>
</div>
<div id="list"></div>
<%= link_to (fa_icon 'plus').to_s + 'Add a Qualification', '#', :id => 'add-to-list' %>
and finally, with strong parameters,
def profile_params
params.require(:profile).permit(:user_id, :first_name, :last_name, degrees_attributes:[:id, :level, :description])
end
Note that I had already set-up degree table with 2 extra fields for polymorphic association:- "degreeable_id" & "degreeable_type" and when entering in DB, the two fields were automatically filled with newly created profile_id and 'Profile'(the name of the model which is associating polymorphically with degree).
The trick in numerous.js is creating each nested form record(here degree) with unique temporary id such as Time.now.to_i so now each degree record created/destroyed at client side will have a diff degree_attribute 'id'. Hope it helps others.

Related

Dynamic Forms with Cocoon - undefined method `reflect_on_association' for NilClass:Class:

im using the cocoon gem to build some dynamic form in which i can add new text fields. Ive read others people same problem but i dont know what im doing wrong, i know it has to be something with the associations but i dont seem to get it.
So these are my models:
class MonitorCategory < ActiveRecord::Base
validates :operation, presence: true
attr_accessor :oid, :oid2, :oids, :snmp_oper, :custom_tab_name, :custom_tab_unit, :redfish, :ipmi
has_many :oids
has_and_belongs_to_many :sensors
accepts_nested_attributes_for :oids
class Oid < ActiveRecord::Base
belongs_to :monitor_category
end
And my form:
<%= simple_form_for(:monitor_category, url: create_monitor_category_path, :html => { :remote => true, :method => :post }) do |f| %>
<div id='oids'>
<%= f.simple_fields_for :oids do |oid| %>
<%= render 'oids_fields', :f => oid %>
<% end %>
<div class='links'>
<%= link_to_add_association 'add oid', f, :oids %>
</div>
</div>
with the partial _oids_fields.html.erb:
<div class='nested-fields'>
<%= f.input :oids %>
</div>
What am i doing wrong? Im getting undefined method `reflect_on_association' for NilClass:Class:. Form is okay since i was looking at the page of cocoon and is the same syntax, so i guess it must be something with the associations but i dont really know, im kind of new to the rails world. Maybe since it says nilClass, i need to create a controller for the Oid model in which i make a new method or something? im lost.
Apparently this doesnt work either, i have the same error:
class OidController < ApplicationController
def new
#oid = Oid.new
end
end
thank you for every answer.
edit: just to be more clear, because im very confused.
Before trying to implement this dynamic form, i already have a form which is working correctly. For example, last two fields are these:
<div class="col-md-12">
<%= f.input :oid, label: 'SNMP OID', as: :search, placeholder: 'Output stored in var1.', required: false, novalidate: true, input_html: {data: { autocomplete_source: get_oids_path }} %>
</div>
<div class="col-md-12">
<%= f.input :oid2, label: 'SNMP OID 2', as: :search, placeholder: 'Output stored in var2.', required: false, novalidate: true, input_html: {data: { autocomplete_source: get_oids_path }} %>
</div>
So basically here im storing the values entered on the attribute :oid and :oid2 from the model .
but instead of having these two fields, i want to have only one, and add more dynamically, so i can enter for example 6 values and saved them all on the :oids attribute. Since i was saving the valued on an attribute, i dont know if i have to create a model for Oid, like a did before, and make it belong_to monitor_category. Or if i can just add an attribute :oids to the controller and store all the values in that variable.
The problem is this line
simple_form_for(:monitor_category, url: create_monitor_category_path, :html => { :remote => true, :method => :post }) do |f|
This creates a form for a MonitorCategory but does not set an object. So when you then call f.simple_fields_for there is no object to iterate over the associations.
Normally in the controller you set a #monitor_category instance variable, which is either set to an existing instance (when editing) or a newly created item.
And then you can write:
simple_form_for(#monitor_category, :html => { :remote => true, :method => :post }) do |f|
Rails is smart enough to deduce the url from the object, it will either create a new one or update an existing one.
Is that clear enough?
I think it's because your form is for a monitor_category and the url is pointing to the create_monitor_category_path. But you're showing us the OidController. You would need something like:
class MonitorCategoryController < ApplicationController
def new
#monitor_category = MonitorCategory.new
#monitor_category.oids.build
end
end
This will initialize the parent object and then build the child association. You need to build at least one child for the fields to show up when using fields for.

Rails: Create Model and join table at the same time, has_many through

I have three Models:
class Question < ActiveRecord::Base
has_many :factor_questions
has_many :bigfivefactors, through: :factor_questions
accepts_nested_attributes_for :factor_questions
accepts_nested_attributes_for :bigfivefactors
end
class Bigfivefactor < ActiveRecord::Base
has_many :factor_questions
has_many :questions, through: :factor_questions
end
and my join-table, which holds not only the bigfivefactor_id and question_id but another integer-colum value.
class FactorQuestion < ActiveRecord::Base
belongs_to :bigfivefactor
belongs_to :question
end
Creating an new Question works fine, using in my _form.html.erb
<%= form_for(#question) do |f| %>
<div class="field">
<%= f.label :questiontext %><br>
<%= f.text_field :questiontext %>
</div>
<%= f.collection_check_boxes :bigfivefactor_ids, Bigfivefactor.all, :id, :name do |cb| %>
<p><%= cb.check_box + cb.text %></p>
<% end %>
This let's me check or uncheck as many bigfivefactors as i want.
But, as i mentioned before, the join model also holds a value.
Question:
How can I add a text-field next to each check-box to add/edit the 'value' on the fly?
For better understanding, i added an image
In the console, i was able to basically do this:
q= Question.create(questiontext: "A new Question")
b5 = Bigfivefactor.create(name: "Neuroticism")
q.bigfivefactors << FactorQuestion.create(question: q, bigfivefactor: b5, value: 10)
I also found out to edit my questions_controller:
def new
#question = Question.new
#question.factor_questions.build
end
But i have no idea how to put that into my view.
Thank you so much for your help!
Big Five Factors model considerations
It looks like your Bigfivefactors are not supposed to be modified with each update to question. I'm actually assuming these will be CMS controlled fields (such that an admin defines them). If that is the case, remove the accepts_nested_attributes for the bigfivefactors in the questions model. This is going to allow param injection that will change the behavior sitewide. You want to be able to link to the existing bigfivefactors, so #question.factor_questions.first.bigfivefactor.name is the label and #question.factor_questions.first.value is the value. Notice, these exist on different 'planes' of the object model, so there wont be much magic we can do here.
Parameters
In order to pass the nested attributes that you are looking for the paramater needs to look like this:
params = {
question: {
questiontext: "What is the average air speed velocity of a sparrow?",
factor_questions_attributes: [
{ bigfivefactor_id: 1, value: 10 },
{ bigfivefactor_id: 2, value: 5 } ]
}
}
Once we have paramaters that look like that, running Question.create(params[:question]) will create the Question and the associated #question.factor_questions. In order to create paramaters like that, we need html form checkbox element with a name "question[factor_questions_attributes][0][bigfivefactor_id]" and a value of "1", then a text box with a name of "question[factor_question_attributes][0][value]"
Api: nested_attributes_for has_many
View
Here's a stab at the view you need using fields_for to build the nested attributes through the fields for helper.
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.collection_check_boxes( :bigfivefactor_id, Bigfivefactor.all, :id, :name) do |cb| %>
<p><%= cb.check_box + cb.text %><%= factors.text_field :value %></p>
<% end %>
<% end %>
API: fields_for
I'm not sure exactly how it all comes together in the view. You may not be able to use the built in helpers. You may need to create your own collection helper. #question.factor_questions. Like:
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.check_box :_destroy, {checked => factors.object.persisted?}, '0','1' %> # display all existing checked boxes in form
<%= factors.label :_destroy, factors.object.bigfivefactor.name %>
<%= factors.text_box :value %>
<%= (Bigfivefactor.all - #question.bigfivefactors).each do |bff| %>
<%= factors.check_box bff.id + bff.name %><%= factors.text_field :value %></p> # add check boxes that aren't currently checked
<% end %>
<% end %>
I honestly know that this isn't functional as is. I hope the insight about the paramters help, but without access to an actual rails console, I doubt I can create code that accomplishes what you are looking for. Here's a helpful link: Site point does Complex nested queries

Rails 4 - simple_form and pre-populating fields from url

I'm using simple_form and I'd like to pre-populate several fields in my form. In the link to the form I'm passing several values to params in the URL. The trouble comes in when I either try to pass a value to a field that is an integer or an association. In either case, the field does not pre-populate.
Example below...the first two fields populate fine, but I had to force them to be text fields. Maybe that's ok to push the strings from the url into the field, but ideally I'd be able to use either the integer (f.input) or association (f.association). The second two fields don't pull in the param values from the URL.
Any ideas? Thanks in advance!
NOTE - this is for generating a NEW record in the database and not for editing an existing record.
URL: http://localhost:5000/list/new?event_id=4&user_id=11
<!-- These two fields pre-populate -->
<%= f.text_field :event_id, :value => params[:event_id] %>
<%= f.text_field :user_id, :value => params[:user_id] %>
<br>
<!-- These two fields do NOT pre-populate -->
<%= f.association :event_id, :value => params[:event_id] %>
<%= f.input :event_id, :value => params[:event_id], label: 'Event' %>
PS - I'm listening to GusGus' new album on Spotify while working on this and it's helping a lot. :)
Best practice is pre-populate form not with params directly but with ActiveRecord object.
For example you have an AR class:
class Party < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
Then in your controller:
def new
#party = Party.new(party_params)
end
# use strong params to make your parameter more secure;)
def party_params
params.permit(:event_id, :user_id)
end
and then in your edit view:
<%= simple_form_for #party do |f| %>
<%= f.association :event %>
<%= f.association :user %>
<% end %>

Use accepts_nested_attributes_for to create new records or update existing

Read the big update for the latest information.
Hey everyone,
I've got a many-to-many relationship in a rails app that involves three tables: a user table, an interests table, and a join user_interests table that also has a rating value so a user can rate each of their interests on a 1-10 scale.
I am basically looking for a way for a new user to create their rating when they sign up and edit them at a future date along with any of their profile information at the same time.
I tried to follow this question Rails nested form with has_many :through, how to edit attributes of join model? but the problem I'm having is trying to incorporate a select list into the mix and having multiple interests to rate for the user.
Model Code:
user.rb
has_many :user_interests, :dependent => :destroy
has_many :interests, :through => :user_interests, :foreign_key => :user_id
accepts_nested_attributes_for :user_interests
interest.rb
has_many :user_interests, :dependent => :destroy
has_many :users, :through => :user_interests, :foreign_key => :interest_id, :dependent => :destroy
user_interest.rb
belongs_to :user
belongs_to :interest
View Code:
app/views/user/_form.html.erb
<%= form_for(#user) do |form| %>
... user fields
<%= form.fields_for :user_interests do |ui_form| %>
... loop through ALL interests
<% Interest.all.each do |interest| %>
<%= ui_form.select :rating, options_for_select(1..10) %>
<%= ui_form.hidden_field :interest_id, :value => interest.id %>
<% end %>
<% end %>
<% end %>
I also included the following in the new/edit actions in my controller #user.interests.build.build_interest
The problem I'm running into is that only one interest rating is being passed in the params hash when I want to have multiple. Also I am getting an exception thrown by rails
Interest(#2172840620) expected, got Array(#2148226700)
What tiny detail did I miss or get wrong that is causing the problem?
EDIT:
I found a way to force this to work but it requires manually editing the HTML in chrome developer tools, the :name attribute of my form elements are being generated as user[user_interests_attributes][rating] but if I change it to user[user_interests_attributes][][rating] it will work when I update a record. However I can't manually specify the :name of a form element that is tied to a form object. So what can I do to show that multiple interest ratings are being passed instead of just one that rails thinks?
BIG Update:
I got a semi functional version going with some slight changes:
View code:
<% form.fields_for :user_interests do |ui_form| %>
<p>
<%= ui_form.select :rating, options_for_select(1..5), :selected => :rating %>
<%= ui_form.label :interest_title %>
<%= ui_form.hidden_field :interest_id %>
</p>
<% end %>
Controller code:
def new
#user = User.new
Interest.all.each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
def edit
#user = #current_user
Interest.unrated_by_user_id(#user.id).each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
Now I am able to edit and get my user_interests updated or created if no rating exists, but I get an error that user is empty when I try to create a new user. Also I am unable to access any of the interest attributes in the form to display the interest the user is actually rating. Can anyone help with those caveats?
You only need #user.interests.build because its a has_many relationship. build_interest is for when there is a has_one/belongs_to relationship.
When using fields_for :user_interests you're telling the User model that an instance of one or more user_interest objects will be in the parameters hash when the user is created/updated. The form is not creating or updating any user_interests but it is sending back an array of user_interest_attributes hashes that represent the user_interests for the user the form references. This is an array of user_interests rating values for which no user_interests exist as you reference them in the form which is the reason you get the error.
Since you are passing a range to the select form helper you aren't actually providing any interests to the form for selection. The select will set a value for the rating column in the user_interests table with a value between 1 and 10. No user_interest exists for the rating to be set on even if the user_interests table has a rating column.
passing :multiple => true in the options hash of the select tag will create a multiple select list but I don't think that is what you want. I think you want many items on a page the user can put an interest rating on.
If you do want a user to be able to select many interests this is how to use fields_for with accepts_nested_attributes_for on a has_many :through relationship:
<%= form_for(#user) do |f| %>
<% f.fields_for :interest_ids do |interest| %>
<ul>
<% Interest.all.each do |choice,i| %>
<li class="selection">
<%= interest.check_box [], { :checked => f.object.user_interest_ids.include?(choice.id) }, choice.id, '' %>
<%= interest.label [], choice.name %>
</li>
<% end %>
</ul>
<% end %>
<% end %>

rails: how to add child association through a text field?

I have two models: Company and Person
class Person < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :people
accepts_nested_attributes_for :people, :allow_destroy => true, :reject_if => proc {|attrs| attrs.all? {|k,v| v.blank? } }
end
And my HTML form partial for new and edit actions looks like this:
<% form_for(#company) do |company_f| %>
<p>
<b>Name</b><br />
<%= company_f.text_field :name %>
</p>
<ul>
<%= render :partial => 'person_fields', :collection => #company.people, :locals => {:company_f => company_f} %>
<%= link_to_add_fields(:people, company_f) %>
</ul>
<p>
<%= company_f.submit "Submit" %>
</p>
<% end %>
where "_person_fields" partial looks like this:
<li>
<% company_f.fields_for :people, person_fields do |person_f| %>
<%= person_f.label :name %>
<%= person_f.text_field :name %>
<% end %>
</li>
At the moment, if I typed in person_f.text_fiel :name the name of the person, and hit save, a new Person model with that name gets created. Not what I want at all, I already HAVE that person's Person model in the database, I rather want to ASSOCIATE this person to this company.
Another thing is that I wouldn't mind using the name for human-friendly identification of the person rather than id like this for the "_person_fields" partial
<li>
<% company_f.fields_for :people, person_fields do |person_f| %>
<%= person_f.label :id %>
<%= person_f.text_field :id %>
<% end %>
</li>
this by the way, doesn't work either. when I hit submit, nothing happens. nothing gets saved or changed or anything.
So I thought, just for the sake of experiment, say I did use id's for identification for a Person model, (so that I don't have to go in to autocomplete with a hidden id field which I am using for another project. I hate it). All I want is: go to a new/edit Company page, there's a bunch of textfields for me to type in ids of people, and save and then these people are then associated with the company. I mean, it's exactly like
people = Person.find(1,2,3)
#=>["romeo","juliet","henry"]
company = Company.first
#=>["Shakespeare Co."]
company.people<<people
company.people
#=>["romeo","juliet","henry"]
And it'd be best if I didn't have to use select menus because eventually if the project takes off and I have a thousand people, that's too big for any select menu. I know then I will have to use autocomplete + hidden id field that gets set when a person's name is chosen.
Thanks!!
accepts_nested_attributes_for :people defines people_attributes and people_attributes= methods in your Company model. Those two methods are used when you have fields_for :people in form. As stated in documentation (read whole page, not only method definition):
Nested attributes allow you to save attributes on associated records through the parent.
It doesn't work to associate two objects, that can be done without it, you still have people method in Company.
Formtastic for example uses select with multiple attribute (you can check more than one option with ctrl) or list of check_boxes (easier for normal user, don't have to touch keyboard).
If you want to use autocomplete, it could be done, but you need to append your ids to people[] array (don't remember format right now, I'll check later. You can check its format when you create attribute in your form for :people I think)
Edit:
I think I know hot to do it simple way (without autocomplete, simple html only).
In your companies/edit (or new) view place:
<p>
<%= company_f.label :people %>
<%= company_f.collection_select :person_ids, Person.all, :id, :name, {}, {:multiple => true} %>
</p>
That will allow you to select multiple people to company (with Ctrl). Params generated by this part transfered to your controller should now look like:
"company"=>{"name"=>"CompanyA", "person_ids"=>["1", "3"]}, "commit"=>"Update", "id"=>"4"
If you want to show people with checkboxes I managed to get it with this (I copied part of markup from formtastic):
<ul>
<% Person.all.each do |person| %>
<li>
<%= check_box :person, :id, {:name => "company[person_ids][]", :checked => #company.people.include?(person) }, person.id, nil %>
<%= person.name %>
</li>
<% end %>
</ul>
I had no time today to check how to do it with hidden fields, so you can use autocomplete, but I believe that it should be similar to checkboxes - you need to use something which adds <input type="hidden" name="company[person_ids][]" value="#{person.id}"> for every autocompleted person, and also need to create initial list of people already in company (YOU OVERRIDE THE LIST, not add to list).
If you want field to add or remove (separate fields, sorry) people from company, then this should work:
in Company model:
def add_people=(people_ids)
ids = people_ids.split(/,/).map(&:to_i)
person_ids += ids if ids
end
def add_people
""
end
def remove_people=(people_ids)
ids = people_ids.split(/,/).map(&:to_i)
person_ids -= ids if ids
end
def remove_people
""
end
and in your new/edit view:
<p>
<%= f.label :add_people %><br />
<%= f.text_field :add_people %>
</p>
<p>
<%= f.label :remove_people %><br />
<%= f.text_field :remove_people %>
</p>
Now all you need to do is to find JavaScript for auto-complete, connect it to ids and names of all people (probably PeopleController#index, :format => :json should be good) and tell it to fill those text_fields (can be hidden fields if you would use autocomplete).
This approach should work, because you define virtual attributes in company model, and by assigning to them string in format "1, 2, 6", you add/remove those ids from your collection of associated people

Resources