Rails jQuery autocomplete and dynamic nested fields - javascript

I have an application in which I'm using the gems rails3-jquery-autocomplete and nested_form to suggest existing users to be tagged as contributors to a project entry.
The form view:
<%= f.fields_for :contributors do |contributor|%>
<%= contributor.autocomplete_field :name, autocomplete_user_profile_last_name_projects_path, :update_elements => { :user_id => "#user_id" } %><br>
<%= contributor.hidden_field :user_id %>
<%= contributor.radio_button :contributor_type, 'Colleague' %>
<%= contributor.radio_button :contributor_type, 'Supervisor' %>
<%= contributor.radio_button :contributor_type, 'Client' %>
<%= contributor.link_to_remove "Remove this contributor" %>
<% end %>
<%= f.link_to_add "Add a contributor", :contributors %><br>
I have also overwritten the get_autocomplete_function(parameters) function in order to allow more than one column in my autocomplete, namely the columns first_name and last_name
def get_autocomplete_items(parameters)
items = UserProfile.select("DISTINCT CONCAT_WS(' ', first_name, last_name ) AS full_name, first_name, last_name, id, user_id").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
end
I also have the following line in my Projects controller:
autocomplete :user_profile, :last_name, :full => true, :extra_data => [:first_name, :last_name, :user_id ], :display_value => :fullname
With the `fullname' function simply being:
def fullname
"#{first_name} #{last_name}"
end
What I want to do is to get the value of user_id from the autocomplete items and put its value inside the hidden field, but when I check the server the it outputs something like:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"DYQ7iJeqdwMQrprSGn0WNHXY5iXDZLA5pUd3OlYJ2so=", "project"=>{"creator"=>"1", "title"=>"lesdodis", "contributors_attributes"=>{"0"=>{"name"=>"Test User the 1st", "user_id"=>"", "contributor_type"=>"Client", "_destroy"=>"false", "id"=>"21"}, "1"=>{"name"=>"Test User the 2nd", "user_id"=>"", "contributor_type"=>"Supervisor", "_destroy"=>"false", "id"=>"22"}, "1393677266696"=>{"name"=>"", "user_id"=>"", "_destroy"=>"1"}, "1393677267518"=>{"name"=>"", "user_id"=>"", "_destroy"=>"1"}}, "description"=>"asdad", "tag_list"=>"sdads", "link"=>"asdasd", "gallery_attributes"=>{"screenshots_attributes"=>{"0"=>{"description"=>"", "_destroy"=>"false", "id"=>"10"}}, "id"=>"16"}}, "commit"=>"Update", "id"=>"16"}
where the user_id field inside contributors is left blank. I have already tried manually assigning ids to the fields by using contributor.object.id to assign a unique number to the field name with limited success, as the new dynamically added fields do not have object ids. I am using this form for both creating and updating a project entry, so I would like to ask for help in how to make this work regardless of editing the field or adding a new one.
Update:
After a while of searching I finally got it to work by adding this code
<% field_name = "#{contributor.object_name}[user_id]".gsub(/(\])?\[/, "_").chop %>
<%= contributor.autocomplete_field :name, autocomplete_user_profile_last_name_projects_path, :update_elements => { :user_id => "##{field_name}" } %><br>
I hope this helps anyone who might encounter the same problem.

I've been wrestling with a similar problem as well. In your case, my feeling is that the nested_form helpers are generating unique identifiers which are defeating the "#user_id" selector passed to :update_elements.
You might be able to workaround it by hooking into the event streams for each gem and then manually updating the hidden :user_id for the specific contributor in the form. For the dynamically added contributors, possibly something like:
$(document).on('nested:fieldAdded', function(nf_event) {
var ac_input = nf_event.field.find('.ui-autocomplete-input');
ac_input.bind('railsAutocomplete.select', function(ac_event, data) {
$(this).next('input[type=hidden]').val(data.item.id);
});
});
For your contributors generated through existing associations (ie. editing), you can bind to their autocomplete inputs on $(document).ready(). This might at least get you pointed in the right direction.

After a while of searching I finally got it to work by adding this code
<% field_name = "#{contributor.object_name}[user_id]".gsub(/(\])?\[/, "_").chop %>
<%= contributor.autocomplete_field :name, autocomplete_user_profile_last_name_projects_path, :update_elements => { :user_id => "##{field_name}" } %>
I hope this helps anyone who might encounter the same problem.

Related

Manipulating Inserted Fields from Checkbox and Select via Jquery - Cocoon Rails

I'm having trouble accessing and using the value of a checkbox and then select field for forms dynamically added via the Cocoon Gem for Rails. I've read through many SO posts and the official Cocoon documentation and I just can't seem to get it right.
The idea is that after adding a form via Cocoon, there are hidden fields that only show up if a specific checkbox is :checked, with one of those fields that being a f.select and upon the selection of that value more fields will either show or hide.
_mortgage_fields.html.erb
...
<%= f.form_group :misc_mortgage do %>
<%= f.check_box :misc_mortgage, label: "Miscellaneous Mortgage" %>
<% end %>
<%= f.select :misc_mortgage_context, [["Assignment", "Assignment"], ["Subordination", "Subordination"], ["Modification", "Modification"]],
{ label: "Miscellaneous Mortgage Type" }, wrapper: { class: 'mtgHidden' }%>
<%= f.text_field :reference_mortgage, class: 'form-control', wrapper: { class: 'mtgHidden' } %>
<%= f.text_field :subordinated_mortgage, class: 'form-control', wrapper: { class: 'Subordination' } %>
<%= f.text_field :modification_amount, class: 'form-control', wrapper: { class: 'Modification' } %>
...
The top level form is wrapped with <div id="mtgForm">..</div>
cocoonoptions.js
$(document.ready(function() {
$('#mtgForm').on('cocoon:after-insert', function(e, misc_checkbox) {
alert('getting there');
$(misc_checkbox).find('input[type=checkbox][id*="+misc_mortgage"]').change(function() {
alert('almost there');
if ($(this).is(':checked'))
alert('there!');
$('.mtgHidden').show();
else
$('.mtgHidden').hide();
});
$(misc_checkbox).find("select[name*='misc_mortgage_context']").change(function() {
var mtg = $(this).val();
if (mtg == "Subordination") {
$('.Subordination').show();
$('.Modification').hide();
}
else if (mtg == "Modification") {
$('.Modification').show();
$('.Subordination').hide();
}
else {
$('.Subordination').hide();
$('.Modification').hide();
}
});
});
The wrapper: { ... } fields are set to display: none by CSS and then shown or hidden according to the above values via JS. This same code works (without the cocoon:after-insert part of course) on a static HTML page for adding a single item without the necessity of adding multiple items at once like Cocoon is beautifully made to do.
I've tried the code above many different ways based on different posts or sites I've found online but I can only seem to get the first test alert to fire. Including misc_checkbox.find('...') without the $(...) wrapper.
Am I going about this the wrong way or is my code just incorrect? Thanks in advance for any and all help!
Update
Of course as soon as I post the question I figured it out. The + in [id*="+misc_mortgage"] was throwing it off and I wasn't loading cocoonoptions.js correctly. Going to leave this question up so maybe it will help someone in the future.
So my code was almost correct. Once I changed
$(misc_checkbox).find('input[type=checkbox][id*="+misc_mortgage"]')
to $(misc_checkbox).find('input[type=checkbox][id*="misc_mortgage"]') and loaded the JS via
<% content_for :javascript do %>
<script type="text/javascript">
</script>
<% end %>
function at the bottom of view, everything worked.

FIlter collection_select by value in coffeescript

Well I have this coffeeScript:
jQuery ->
$('#cmbPab').change ->
ubis = $('#cmbUbi').html()
pabellon = $(this).val()
options = $('#cmbUbi').filter("FilterByValue")
I want it to filter options by value, I already saw railcats stuff but he filters by a different way and I need to do it by value cause value is my id
Here is the cod of both collection_select where cmbPab is my main combo:
<div class="form-group">
<%= f.label :Pabellon %>
<%= f.collection_select :idubicacion, Mtopabellon.all,
:codpabellon, :nombre, {prompt: 'Seleccione un pabellón'},
:class=>'form-control', :id => 'cmbPab'%>
</div>
<div class="form-group">
<%= f.label :Ubicacion %>
<%= f.collection_select :idubicacion, CrUbicacion.all, :id,
:nombre, {prompt: 'Seleccione una ubicación'}, :class=>'form-
control', :id => 'cmbUbi' %>
</div>
I believe you're after something along these lines:
$('#cmbPab').change ->
displayFilterFn = (i, el) -> $(el).val() == 'something?' // do your filtering
$ubis = $('#cmbUbi')
$ubis.children().hide()
$ubis.children().filter(displayFilterFn).show()
Without knowing the specifics of how you want to filter your options, other than that it is based on the value, I can't provide you a filtering function implementation (displayFilterFn above). The basic idea is to first hide all options, and then show the ones that match your filter.
For more info on the .filter(), see the jQuery API Documentation.
Here is the answer guys! I think this is the best way to do it, maybe not the easier but it works!
https://kernelgarden.wordpress.com/2014/02/26/dynamic-select-boxes-in-rails-4/

Rails Merge Form Fields

I have a Rails 3.2.18 app in which I'm doing a simple create/update action on a model.
There's a field called sms which I used ActionMailer to send a message to a person's phone in the form of 2812222222#vtext.com. Currently I manually type this in which is fine, but I want other's to be able to enter a phone number, select a carrier and have these two items merge into the proper format in the sms field again being 2812222222#vtext.com. Makes it a lot more user-friendly than having to know the full sms-to-email address by heart.
I assume I can do most of this via JS/JQuery, but I'm not sure on how to get started.
I'd like them to be able to enter the phone number in a 281-555-4444 format and have the carrier select as a drop-down of Tmobile, Verizon, etc and substitute #tmomail.net, #vtext.com, etc, strip (regex) the phone number and merge it with the carrier (#vtext.com) into the sms field.
If anyone can point me in the right direct, it would be appreciated.
Update:
I figured I could use a helper method to define the carriers:
def phone_carriers
{
"All Tell" => "#message.alltel.com",
"AT&T" => "#txt.att.net",
"Boost" => "#myboostmobile.com",
"Cellular South" => "#csouth1.com",
"Centennial Wireless" => "#cwemail.com",
"Cincinnati Bell" => "#gocbw.com",
"Cricket Wireless" => "#sms.mycricket.com",
"Metro PCS" => "#mymetropcs.com",
"Powertel" => "#ptel.net",
"Qwest" => "#qwestmp.com",
"Rogers" => "#pcs.rogers.com",
"Sprint" => "#messaging.sprintpcs.com",
"Suncom" => "#tms.suncom.com",
"T-Mobile" => "#tmomail.net",
"Telus" => "#msg.telus.com",
"U.S. Cellular" => "#email.uscc.net",
"Verizon" => "#vtext.com",
"Virgin Mobile USA" => "#vmobl.com"
}
end
Add attr_accessor :carrier to the Person model
Then in the form do something like this:
<%= f.input :phone, placeholder: "555-555-5555", required: true, input_html: {required: true} %>
<%= f.select :carrier, phone_carriers.map{ |k, v| [k, v] }, {include_blank: true}, {placeholder: "Select your phone carrier", class: "select2"} %>
But I'm still having problems figuring out how to strip the phone number to 5555555555 and merging the :phone field with the :carrier object into the :sms field automatically (preferably client side)
After doing a lot of research and breaking things, I've come up with the following to make this work. The model and form only show what changes I've made, not the full form or model.
Form
<%= f.label 'Cell_Phone'%>
<%= f.text_field :phone, placeholder: "xxx-xxx-xxxx", required: true %>
<%= f.label :carrier, "SMS Notifications", class: "control-label" %>
<%= f.select :carrier, phone_carriers.map{ |k, v| [k, v] }, {include_blank: true}, {placeholder: "Select your phone carrier", class: "select2"} %>
<%= f.hidden_field :sms %>
Model
attr_accessible :carrier
attr_accessor :carrier
CoffeeScript
$ ->
$("#person_carrier").on "change", update_sms_address
$("#person_phone").on "keyup", update_sms_address
update_sms_address
update_sms_address = ->
digits = $("#person_phone").val().match(/\d+/g)
phone = digits.join("") if digits
carrier = $("#person_carrier").val()
if phone? && phone.length == 10 && carrier
sms = phone + carrier
$("#person_sms").val(sms)
$("#person-sms-number").html(sms)
else if !carrier
$("#person_sms").val("")
$("#person-sms-number").html("Choose a carrier to receive SMS notifications")
else
$("#person_sms").val("")
$("#person-sms-number").html("Invalid phone number")
This seems to work when entering a phone number in the format of 281-444-4444 as it strips the number down and joins it with the value from the carrier array and sets the hidden :sms field appropriately.
The only problem I have now is since I'm using a virtual attribute for the carrier, if I go to edit the person again it doesn't retain the carrier select and leaves it blank. If I submit the form without changing any info it will retain the :sms field, but it could be confusing since the :carrier is not an actual field that is remembered by the database but instead a virtual attribute.
Is there a better way to go about doing this?
If you used the standard javascript string library, wouldn't something like
function prepare(str){
str2 = str.replace("(","").replace(")","").replace("-","")
carrier = getCarrier //assuming this implemented by you somewhere
return (st2.concat(carrier))
}
I would start by creating a carriers table:
rails generate model Carrier name:string email_domain:string
And referencing it in the Person model by adding the following to your migration:
change_table :people do |t|
t.references :carrier
end
add_index :people, :carrier_id
And the following to your People model:
belongs_to :carrier
validates_presence_of :carrier
def sms
carrier ? "#{phone}##{carrier.email_domain}"
end
def phone_with_dashes
"#{phone[0..2]}-#{phone[3..5]}-#{phone[6..9]}"
end
def phone_with_dashes=(_phone_with_dashes)
self.phone = _phone_with_dashes.gsub(/-/ ,'')
end
Now, your view (I'll leave the formatting to you):
Phone: <%= f.phone_field :phone_with_dashes %>
Carrier: <%= f.collection_select :carrier_id, Carrier.all, :id, :name %>
<div id='person-sms-number'>
<%= f.object.sms %>
</div>
Lastly, the Coffeescript: The only thing that has to change here is you no longer need the '#person_sms' input since you are no longer persisting the value directly--you are generating it dynamically from other fields.

Escape_javascript problem

I have a set of dependent dropdown menus on my user/edit page. The user selects the parent dropdown, which then limits the selections available in the child dropdown. The user can add multiple additional sets of parent/child dropdowns. Product is the parent. Variety is the child.
This all works great unless a variety is entered into the database that includes double quotes. When that happens, it breaks the javascript and the trigger to render the partial doesn't work at all.
Single quotes do not cause this problem, nor do any other characters that I've tried. So this problem is specific to double quotes. I thought that escape_javascript was the answer here, but I've tried placing it in many different places in the code and nothing has worked. It's very possible that I just don't know exactly where the helper and it's parenthesis are supposed to go.
The api documentation is terrible. It says escape_javascript(). That's not helpful for me. Similarly, there isn't much clear guidance online. I've searched for hours.
Here are the relevant parts of my code:
Users#edit.html.erb
<%= render :partial => 'season', :collection => #user.seasons %>
Users#_season.html.erb
<% fields_for prefix, season do |season_form| -%>
<%= error_messages_for :season, :object => season %>
Product: <%= season_form.collection_select :product_id, Product.find(:all, :order =>'name'), :id, :name, {:prompt => "Select Product"}, {:onchange => "collectionSelected(this);"} %>
<% varieties = season.product ? season.product.varieties : Variety.all %>
<%= season_form.select :variety_id, options_from_collection_for_select(varieties, :id, :name, season.variety_id), :prompt => "This is optional" %>
Javascripts#dynamic_varieties.js.erb
var varieties = new Array();
<% for variety in #varieties -%>
varieties.push (new Array (<%=h variety.product_id %>, "<%=h variety.name %>", <%=h variety.id %>));
<% end -%>
function collectionSelected(e) {
product_id = e.getValue();
options = e.next(1).options;
options.length = 1;
varieties.each(function(variety) {
if (variety[0] == product_id) {
options[options.length] = new Option(variety[1], variety[2]);
}
});
}
Assuming you're just doing some escaping wrong (and that rails does it correctly), you could try something like:
var varieties = <%= #varieties.map { |v| [v.product_id, h(v.name), v.id] }.to_json %>

Dynamically Generated Select Menus Not Holding State in Edit View

I'm following Railscast 88 to create a dynamic dependent dropdown menu. http://railscasts.com/episodes/88-dynamic-select-menus
I'm rendering these dropdowns inside a partial that I'm using in a multi-model form. The form I'm using follows the Advanced Rails Recipes process by Ryan Bates. Because I'm rendering the dropdown inside a partial, I had to depart from strictly following the Railscast code. On the Railscast link provided above, comments 30-31 and 60-62 address these issues and provide an approach that I used.
For new records, everything is working great. I select a parent object from the dropdown, and the Javascript dynamically limits the child options to only those items that are associated with the parent I selected. I'm able to save my selections and everything works great.
The problem is that when I go back to the edit page, and I click on the child selection dropdown, the constraints tying it to the parent object are no longer in place. I'm now able to select any child, whether or not it's connected to the parent. This is a major user experience issue because the list of child objects is just too long and complicated. I need the child options to always depend on the parent that is selected.
Here's my code:
Controller#javascripts
def dynamic_varieties
#varieties = Variety.find(:all)
respond_to do |format|
format.js
end
end
Views#javascripts #dynamic_varieties.js.erb
var varieties = new Array();
<% for variety in #varieties -%>
varieties.push(new Array(<%= variety.product_id %>, '<%=h variety.name %>', <%= variety.id %>));
<% end -%>
function collectionSelected(e) {
product_id = e.getValue();
options = e.next(1).options;
options.length = 1;
varieties.each(function(variety) {
if (variety[0] == product_id) {
options[options.length] = new Option(variety[1], variety[2]);
}
});
}
Views#users #edit.html.erb
<% javascript 'dynamic_varieties' %>
<%= render :partial => 'form' %>
View#users #_form.html.erb
<%= add_season_link "+ Add another product" %>
<%= render :partial => 'season', :collection => #user.seasons %>
view#users #_season.html.erb
<div class="season">
<% new_or_existing = season.new_record? ? 'new' : 'existing' %>
<% prefix = "user[#{new_or_existing}_season_attributes][]" %>
<% fields_for prefix, season do |season_form| -%>
<%= error_messages_for :season, :object => season %>
<div class="each">
<p class="drop">
<label for = "user_product_id">Product:</label> <%= season_form.collection_select :product_id, Product.find(:all), :id, :name, {:prompt => "Select Product"}, {:onchange => "collectionSelected(this);"} %>
<label for="user_variety_id">Variety:</label>
<%= season_form.collection_select :variety_id, Variety.find(:all), :id, :name, :prompt => "Select Variety" %>
</p>
<p class="removeMarket">
<%= link_to_function "- Remove Product", "if(confirm('Are you sure you want to delete this product?')) $(this).up('.season').remove()" %>
</p>
</div>
<% end -%>
Here's your culprit:
<%= season_form.collection_select :variety_id, Variety.find(:all),
:id, :name, :prompt => "Select Variety" %>
Works perfectly on a new record because it's showing everything, and gets overwritten when the select changes on the other select box.
You need to do something like this:
<% varieties = season.product ? season.product.varieties : Variety.all %>
<%= season_form.select :variety_id,
options_from_collection_for_select(varieties, :id,
:name, season.variety_id), :prompt => "Select Variety" %>
Which will use only the Varieties linked to season.product. If season.product doesn't exist it lists all of them. It will also automatically select the right one if the existing record had a variety_id.
It also wouldn't hurt to change.
<%= season_form.collection_select :product_id, Product.find(:all),
:id, :name, {:prompt => "Select Product"},
{:onchange => "collectionSelected(this);"} %>
to
<%= season_form.select :product_id,
options_from_collection_for_select(Product.find(:all),
:id, :name, season.product), {:prompt => "Select Product"},
{:onchange => "collectionSelected(this);"} %>
Which will select the proper product on page load. This second part is essentially the Rails way of doing what BYK's first suggestion was. However, given the nature of the onchange method given to the select box, this line on its own would not solve the problem. It would just enhance the user experience by highlighting the product associated with the season.
I think you have two options:
Give one of the products(or simply the first element of the product list) a "selected" attribute which will force the browser to select that one always.
Trigger the "collectionSelected" function on "dom ready" or "window.onload" with giving the product list selectbox as its parameter.
And a note: never, ever trust JavaScript to force the user to send proper data to the server.

Categories