jquery autocomplete for dynamically inserted elements with unknown id - javascript

We have autocomplete working on static form:
$(function() {
return $('#bom_part_name_autocomplete').autocomplete({
minLength: 1,
source: $('#bom_part_name_autocomplete').data('autocomplete-source'),
select: function(event, ui) {
$(this).val(ui.item.value);
},
});
});
The autocomplete above fills in the part name when user choose one. Now we would like to add autocomplete to dynamically inserted elements. Here are a few things we need to consider:
element id is unknown until after insertion
There may be more than one element inserted on the same form.
the id for dynamically inserted elements always starts 'bom_' and ends with '_part_name_autocomplete'. autocomplete needs to match id with this type of pattern.
The digits in ids are different from element to element. For example, the id for 1st insertion may be 'bom_123456789_part_name_autocomplete',
the id for 2nd insertion could be 'bom_123456987_part_name_autocomplete'
None of online examples we found has unknown element ids. Is it possible to do autocomplete on dynamic elements with unknown ID? If it can be done, an example would be greatly appreciated.
UPDATE: here is the rails code to create element:
def link_to_add_fields(name, f, association)
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render :partial => association.to_s, :locals => {:f => builder, :i_id => 0}
end
link_to_function(name, "add_fields(this, \"#{association}\", \"#{j fields}\")")
end
Here is the javascript for add_fields():
function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp("new_" + association, "g");
$(link).parent().before(content.replace(regexp, new_id));
}

Well, after your elements are inserted you can do:
$('[id^=bom_][id$=_part_name_autocomplete]').each(function(){
$(this).autocomplete({ // ...
})
Its easier to just add it on creation though

Referencing an element by ID is not the only way to select it.
You can add the autocomplete when you create the element:
function create_element(data) {
var field = $("<input/>")
field.data('autocomplete-source', data)
$(field).autocomplete({
minLength: 1,
source: $(field).data('autocomplete-source'),
select: function(event, ui) {
$(this).val(ui.item.value);
},
});
return field
}

Related

Rails + StimulusJS dynamically created select menu doesn't work

I'm dynamically adding fields to my form. In case of a standard text field it works fine with Rails + StimulusJS with an <template> element.
I'm using MaterializeCSS for styling and if I try to do the same thing with a select menu, it breaks and it seems the innerHTML I get back in my JS code doesn't match the code inside the template-tag. So I decided to use a div instead which I duplicate.
The view code (the relevant part):
<div data-nested-form-target="template">
<%= f.fields_for :stations, department.stations.build, child_index: "NEW_RECORD" do |station| %>
<%= render "admin/departments/stations/station_fields", f: station %>
<% end %>
This is the template I duplicte in my Stimulus controller.
This is what the first (and working) select menu looks like in HTML:
So the next step was to change the ID'S of the <ul> and the two <li> elements + the data-target element to target the right select menu.
What I ended up was this JS-Code, which is indeed adding a second select menu with the right styles, but it is not clickable and doesn't show options, despite they exist in the HTML markup and the ID's differ from the first one:
add_association(event) {
event.preventDefault()
let random_id = this.generate_random_id()
let content = this.templateTarget.cloneNode(true)
content.getElementsByTagName("ul").item(0).setAttribute("id", random_id)
content.getElementsByClassName("dropdown-trigger").item(0).setAttribute("data-target", random_id)
let list_elements = Array.from(content.getElementsByTagName("ul").item(0).querySelectorAll("li"))
list_elements.forEach((item) => {
let rnd = this.generate_random_id()
item.setAttribute("id", rnd)
})
let html = content.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
this.linksTarget.insertAdjacentHTML("beforebegin", html)
console.log(html)
let collection = this.basicTarget.getElementsByClassName("nested-fields")
let element = collection[collection.length - 1].getElementsByClassName("animate__animated")[0]
element.classList.add(this.fadeInClass)
}
Now it looks like this and I can't figure out how to make this thing working:
We're using the same stack (Rails, Stimulus.js, Materialize) with no problem. One thing you have to do after updating the select is re-initialize the Materialize select. For example, this is a general purpose nested select controller in which different sub-selects become active based on the parent select:
import { Controller } from 'stimulus';
import Materialize from 'materialize-css';
export default class extends Controller {
static targets = [
'childSelect',
]
parentSelectChanged (e) {
const selectedParentMenu = e.target.value;
this.childSelectTargets.forEach(selectElement => {
if (selectElement.dataset.parentMenuName === selectedParentMenu) {
selectElement.disabled = false;
selectElement.value = '';
} else {
selectElement.disabled = true;
}
Materialize.FormSelect.init(selectElement);
});
}
}
And here is the corresponding Haml:
.input-field.col
= select_tag :category,
options_for_select(grouped_conditions.keys.sort, selected_parent_menu_name),
include_blank: 'Select One...',
data: { action: 'nested-select#parentSelectChanged' }
.input-field.col.hide-disabled
- grouped_conditions.each do |parent_menu_name, child_conditions|
= f.select :condition_id,
options_for_select(child_conditions.sort_by(&:menu_name).map{ |tc| [tc.menu_name, tc.id] }, selected_condition_id),
{ include_blank: 'Select One...' },
disabled: selected_parent_menu_name != parent_menu_name,
data: { 'nested-select-target' => 'childSelect', 'parent-menu-name' => parent_menu_name }
= f.label :condition_id

calling .val() on date_select element returning undefined?

I'm trying to obtain the selected date from a date_select:
<%= form_for(#newevent) do |f| %>
<%= f.date_select :day, { id: "date-select"} %>
<button id="check-button" type="button">Check</button>
<% end %>
using JQuery/Javascript:
$(document).on('click', "#check-button", function(){
var selectedDate = $("#date-select").val();
alert(selectedDate);
var checkList = []; //creates array to store customer ids
$("#check-list li input").each(function(){ //for each listed colleague...
if( $(this).is(":checked")){ //if check-box is ticked
checkList.push($(this).val()); //add id to list
}
});
$.ajax({
url: '/events/check',
data: {checkList: checkList , selected_date: selectedDate },
method: "POST"
}
);
});
for reference here is where :day is defined in my migration file:
class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.timestamps
t.references :calendar, foreign_key: true
...
t.date :day
...
end
However, on alert the returned value is "undefined". Why is this happening? I would like it to return the a value in the form YYYY-MM-DD. Apologies if this is obvious, but I am new to programming and can't seem to fix this one myself
Date_select, according to the rails api "Returns a set of select tags (one for year, month, and day)". Your javascript is going to have to deal with three separate selects and construct a date from them. Look at your generated HTML to get the id's.

How to pre-populate dynamically added form inputs in Rails

I have an edit.html.erb form for #profile instances. Normally when I load the form, the inputs inside the form are pre-populated with the corresponding attribute of the instance.
e.g. <%= f.text_field :name %> would pre-populate its value from #profile.name.
I would like to add additional fields on runtime using jQuery that allows users to input additional data with additional dynamically added inputs.
i.e. #profile.subjects is a hash containing the subjects and their descriptions (for example it could be {"math" => "I teach calculus", "science" => "I have a physics degree"} )
I retrieve a list of subjects from #profile.subjects, and insert textarea elements into the form for each subject using append() in jQuery.
So if #profile.subjects contains "math" and "science", I would do the following in jQuery:
var subject = *string containing subject*;
parent.append("<textarea id='profile_" + subject + "_desc' name='profile[" + subject + "_desc]'></textarea");
This would imitate creating the fields <%= f.text_area :math_desc %> and <%= f.text_area :science_desc %> to my knowledge.
However, when I pass in the attributes math_desc and science_desc to the instance variable #profile in my controller, the inputs do not pre-populate with their values unlike the static form inputs.
I can access #profile.math_desc and #profile.science_desc in the view, but I would like the inputs to have these values upon the view loading.
I do not know how I would add ruby variables to the append() argument with a variable as the attribute name.
e.g. append("<textarea><%= #profile.send('math_desc') %></textarea>") works, but append("<textarea><%= #profile.send('" + subject + "_desc') %></textarea>") does not.
EDIT 1:
Here is how I assigned additional attributes to my instance:
# NOTE: #profile.subjects contains a string representation of {"Math" => "I teach calculus", "Science" => "I have a physics degree", ...}
def edit
#tutor = current_tutor
#profile = #tutor.profile
# Call method to parse subjects JSON
subject_parsing(#profile)
end
private
# Decode JSON subject field into a hash
def subject_parsing(profile)
unless profile.subjects.blank?
# Decode subjects into a hash
parsed_subjects = ActiveSupport::JSON.decode(profile.subjects)
# Extract subject names from hash into a string separated by commas
profile.subject_names = parsed_subjects.keys.join(', ')
parsed_subjects.each do |subj, desc|
subj = subj.downcase
profile.class_eval do
attr_accessor "#{subj}_desc"
end
profile.send("#{subj}_desc=", desc)
end
end
end
So my workaround was to use the gon gem to send a Rails hash to Javascript then calling the subject descriptions according to the subject_names array.
Added this to the controller (see EDIT 1 code)
# Decode JSON subject field into a hash
def subject_parsing(tutor_profile)
unless tutor_profile.subjects.blank?
# Decode subjects into a hash TODO: change this because it is very slow
gon.subjects = ActiveSupport::JSON.decode(tutor_profile.subjects)
# Extract subject names from hash into a string separated by commas
tutor_profile.subject_names = gon.subjects.keys.join(', ')
end
end
Then ended up with this in the Javascript:
var subjectNamesInput = $('#tutor_profile_subject_names');
// Initialize subject description input boxes
$.each(subjectNamesInput.tagsinput('items'), function(i, subject){
updateSubjectInputs(subject, 'add');
});
function updateSubjectInputs(subject, action){
var parent = $('#subject-description-inputs');
var subject = escape(subject);
var text = "";
if (gon.subjects[subject] != undefined)
text = gon.subjects[subject];
if (action == 'add'){
parent.append("<textarea id='tutor_profile_" + subject.toLowerCase() + "_description' name='tutor_profile[" +
subject.toLowerCase() + "_description]' class='form-control form-text-area' rows='5' placeholder='Add some detail about your experience with " + subject +
" (optional)'>" + text + "</textarea>");
}
else if (action == 'remove'){
$('#tutor_profile_' + subject.toLowerCase() + '_description').remove();
}
}
This was in my view:
<%= f.text_field :subject_names, class: 'form-control tagsinput typeahead', placeholder: 'Subjects you teach' %>
<div id="subject-description-inputs">
</div>

Using jQuery to update multiple objects

I have a profile page where I render all of the user's lists using a partial:
users/show.html.erb
<%= render #lists %>
For each list I show how many wishes are associated:
lists/_list.html.erb
<p class="muted" id="wishes_count"><%= pluralize(list.wishes.count, "wish") %></p>
I'm using drag and drop to allow the user to move wishes from one list to another using jQuery sortable.
Right now, I'm using the following code:
wishes.js.erb
$(document).ready(function(){
$('.user_list_wishes').sortable({
connectWith: ".user_list_wishes",
items: ".user_list_wish",
placeholder: "sortable_placeholder",
update: function(e, ui)
{
if (this === ui.item.parent()[0])
{
item_id = ui.item.data('item-id');
list_id = $(this).data('list-id');
position = ui.item.index();
$.ajax({
type: 'POST',
url: $(this).data('update-url'),
dataType: 'json',
data: { id: item_id, wish: { row_order_position: position, list_id: list_id } }
}),
$("#wishes_count").html('<%= pluralize(list.wishes.count, "wish") %>')
}
}
})
})
This line of code:
$("#wishes_count").html('<%= pluralize(list.wishes.count, "wish") %>')
makes my application throw a NoMethodError undefined method 'wishes' for nil:NilClass
I believe it has something to do with how I render lists and reference them individually in my javascript?
Any suggestions on how to solve this problem is much appreciated!
You must handle the scenario where list is null using the following condition:
$("#wishes_count").html('<%= pluralize(list ? list.wishes.count : 0, "wish") %>')
I think the problem could be in your render call - according to the documentation:
If you have an instance of a model to render into a partial, you can use a shorthand syntax:
<%= render #customer %>
Assuming that the #customer instance variable contains an instance of the Customer model, this will use _customer.html.erb to render it and will pass the local variable customer into the partial which will refer to the #customer instance variable in the parent view.
So check that this is the case - do you have a partial called _lists.html.erb and a parent variable called list?

jquery autocomplete in variable length list

Trying to figure out how to do this, using Sanderson begincollectionitems method, and would like to use autocomplete with a field in each row.
I think I see how to add a row with an autocomplete, just not sure the approach for existing rows rendered with guid.
Each row has an of field that the user can optionally point to a record in another table. Each autocomplete would need to work on the html element idfield_guid.
I'm imagining using jquery to enumerate the elements and add the autocomplete to each one with the target being the unique of field for that row. Another thought is a regex that maybe let you enumerate the fields and add autocomplete for each in a loop where the unique field id is handled automatically.
Does that sound reasonable or can you suggest the right way? Also is there a reasonable limit to how many autocomplete on a page? Thanks for any suggestions!
Edit, here's what I have after the help. data-jsonurl is apparently not being picked up by jquery as it is doing the html request to the url of the main page.
$(document).ready(function () {
var options = {
source: function(request, response) {
$.getJSON($(this).data("jsonurl"), request, function (return_data) {
response(return_data.itemList);
});
},
minLength: 2
};
$('.ac').autocomplete(options);
});
<%= Html.TextBoxFor(
x => x.AssetId,
new {
#class = "ac",
data_jsonurl = Url.Action("AssetSerialSearch", "WoTran", new { q = Model.AssetId })
})
%>
And the emitted html look okay to me:
<input class="ac" data-jsonurl="/WoTran/AssetSerialSearch?q=2657" id="WoTransViewModel_f32dedbb-c75d-4029-a49b-253845df8541__AssetId" name="WoTransViewModel[f32dedbb-c75d-4029-a49b-253845df8541].AssetId" type="text" value="2657" />
The controller is not a factor yet, in firebug I get a request like this:
http://localhost:58182/WoReceipt/Details/undefined?term=266&_=1312892089948
What seems to be happening is that the $(this) is not returning the html element but instead the jquery autocomplete widget object. If I drill into the properties in firebug under the 'element' I eventually do see the data-jsonurl but it is not a property of $(this). Here is console.log($this):
You could use the jQuery UI Autocomplete plugin. Simply apply some know class to all fields that require an autocomplete functionality as well as an additional HTML5 data-url attribute to indicate the foreign key:
<%= Html.TextBoxFor(
x => x.Name,
new {
#class = "ac",
data_url = Url.Action("autocomplete", new { fk = Model.FK })
})
%>
and then attach the plugin:
var options = {
source: function(request, response) {
$.getJSON($(this).data('url'), request, function(return_data) {
response(return_data.suggestions);
});
},
minLength: 2
});
$('.ac').autocomplete(options);
and finally we could have a controller action taking two arguments (term and fk) which will return a JSON array of suggestions for the given term and foreign key.
public ActionResult AutoComplete(string term, string fk)
{
// TODO: based on the search term and the foreign key generate an array of suggestions
var suggestions = new[]
{
new { label = "suggestion 1", value = "suggestion 1" },
new { label = "suggestion 2", value = "suggestion 2" },
new { label = "suggestion 3", value = "suggestion 3" },
};
return Json(suggestions, JsonRequestBehavior.AllowGet);
}
You should also attach the autocomplete plugin for newly added rows.

Categories

Resources