Jquery multiple selects trigger change - javascript

I have a category tree and I am using Jquery on my page to generate a new category select after a previous category has been chosen.
What I need is, if I pass a list of categories (which are ancestors and children of each other), use the code to perform the value selection (which will call the method that I have written previously).
Here is the code that is responsible to fire upon the category change:
$(document.body).on('change', '.category-select' ,function(event){
var select_rank = parseInt($(event.target).attr('rank'));
var current_category = event.target.value;
select_category(select_rank, current_category, true)
});
When page is rendered there is code that will create the first select. Then on each select change - the select_category function will render a new select (if the category has subcategories).
I am passing a list of categories in an array called cat_list. If it is set, I need the selects to be triggered on last select:
if(cat_list != null){
for(cat_id in cat_list){
$('.category-select').last().val(cat_id).change();
}
}

You're not iterating over the array correctly. In your code, cat_id is the array index, not the value from the array. Try this:
if (cat_list) {
var last_select = $('.category-select:last');
$.each(cat_list, function(i, cat_id) {
last_select.val(cat_id).change();
});
}

Related

JavaScript issue on filtering text in html element

I've been struggling with the same piece of code for a few days by now...
So for the html part I have this :
<input type="text" id="search_immobilier_ville" name="search_immobilier[ville]">
<div class="collection" id="search-ville-collection"></div>
I have an input where I have to type any city name, then I want to filter the matching cities names into an existing array of cities like this :
let api_results = [['Le Moule',97152],['Lamentin',97189],...]
let ville_input = document.getElementById('search_immobilier_ville');
Then display the matching cities as a list of elements in the div #search-ville-collection.
On each keyup event, I need to perform this action, and update the visual list in real time.
My issue is that my filtering system is messed up, if I search "lam" for example, I can get a city called "lamentin" (pass the test) and another with just the "la" matching like "capesterre-de-marie-galante"
So far, I've done this :
// Previously filled by an API
let api_results = [[name,postalCode],[name,postalCode],...];
ville_input.addEventListener('keyup', () => {
let value = ville_input.value.toUpperCase().trim();
// User input
let input_val = ville_input.value.toUpperCase().trim();
// Filtering through var ville_input
api_results.forEach(el => {
if (el[0].toUpperCase().startsWith(input_val) || input_val.length >= 2 && el[0].toUpperCase().includes(input_val)) {
result_list.style.display = 'block';
// if city not present in the html list, add it
if (document.getElementById(el[1]) === null) {
$(result_list).append(`<a class="collection-item search-ville-results" id="${el[1]}"> ${el[0]} - ${el[1]} </a>`);
}
}
}); // End forEach
/* Looping through the collection child nodes to check
if there are cities that don't match the user input */
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
// Highlight first element of the list
result_list.firstElementChild.classList.add('active');
// empty results div if user input is empty
if (input_val == '') {
result_list.style.display = 'none';
result_list.innerHTML = '';
}
});
This code works PARTIALLY. For example, if I type "lam", I'm supposed to get only one result based on my result set, but check out this scenario :
Typing "l":
Typing "la":
Typing "lam":
(Here you begin to see the issue)
Typing "lame":
I'm sure there's something wrong in my code, but I can't figure out what.
Your problem is with the loop you are using to remove invalid items:
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
children returns a live HTMLCollection, meaning that if you modify it (eg, by removing items) it will update, which will cause issues with your loop. You need to go through the items in a way that will not be affected if the collection changes.
Wrong way:
This is an example of how the loop behaves currently. The button should remove all the items that contain "th", but note how it doesn't get them all and requires multiple clicks:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
for (let item of list.children) {
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
(A) correct way:
One way to loop through the collection in a way that is not affected by items being removed is to start at the last index and go backwards:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
let index = list.children.length
while (index--) {
let item = list.children[index]
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
Better way
As an additional note, you might be better off just clearing the list entirely and using filter to get the matching results and then update the list that way. As it is currently, you are doing a lot of checking to see if the list already contains the item, checking the current list for invalid items, etc. That will affect performance of your UI, especially on lower end devices.
Try to clear out your result_list as the first thing you do inside your keyup event.
result_list.innerHTML = '';
After that, make sure to filter your api_results.
const filteredResults = api_results.filter(result => result[0].toUpperCase().includes(input_val));
console.log(filteredResults); // Sanity check.
filteredResults.forEach(result => /* your old function. */);

How to get the ID of the select-option element with "change" event listener?

I am new into javascript, and I've been working on this "project", but I need some help because I'm stuck. I might've not expressed my self correctly in the title so here it is:
I would like to get the ID of an option element (<select> <option id="#"> </select>) by using the "change" event listener on the <select>. So when I choose for example "Action" from the select dropdown, I'd like that change to trigger a function that will get that element's ID and use it in a function down below. Here's the code that I have so far, which basically does the following:
1.) Gets the genre list;
2.) Then for every item in the response.data.genres, sets a number which corresponds to the length of the array (total 19 items).
3.) If the selected "option" element matches the name of the genre in the array, then it defines the genre ID(the integer) and makes another request to the API in order to list the movies matching that genres ID. Thanks in advance.
//Genres
function genres(){
//API request.
axios.get("https://api.themoviedb.org/3/genre/movie/list?api_key=<API_KEY>&language=en-US")
.then((response)=>{
//console.log(response);
let genres = response.data.genres;
genres.length;
console.log(genres)
for(var i = 0; i < genres.length; i++){
var genresId = response.data.genres[i];
var tag = document.getElementById("Thriller");
console.log(genresId);
if(tag.id === genresId.name){
let genre = genresId.id;
axios.get("https://api.themoviedb.org/3/discover/movie?api_key=<API_KEY>&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres="+genre)
.then((response)=>{
console.log(response);
})
}
}
})
}
So there are two steps if I understand correctly.
1. get the list of genres and fill a selectbox with it.
2. get a list of movies if an option in the selectbox is selected.
The first step, you can do with an innerHTML method. For every genre that is returned, you build a string like <option value='genre'>genre</option>. With innerHTML you add these options to the select box. The value property is what you use to see which option is selected.
Next we add an eventlistener to the dropbox so our script will react to the changes the user makes. The event we're listening for is 'change' and it will trigger the function 'getMovies'. See Mozilla docs for more info. event.target.value will give you the value of the selected option, which you can use as genre id.
Inside this function you will do your second api call to get your movie list.
A simple example without the api calls is this:
let genreDropdown = document.getElementById('genre');
genreDropdown.innerHTML = getGenres();
genreDropdown.addEventListener("change", getMovies);
function getGenres(){
let genres = ['action', 'romcom', 'thriller']; //this would be replaced with the api call to get the genres
let innerHtml = '';
for(var i = 0; i < genres.length; i++){
var option = '<option value='+genres[i]+'>'+genres[i]+'</option>';
innerHtml += option;
}
return innerHtml;
}
function getMovies(event) {
let genre = event.target.value;
alert(genre) //you can replace this with the api call to get the movies.
}
<select id='genre'>
<option>loading...</option>
</select>
The following Code will look for a <select> (its id to be exact) Element and on change it will output the ID of the Direct Child (<option> in this case).
$(document).ready(function(){
$("#myDropdown").on("change", function(){
the_id = $(this).children(":selected").attr("id")
$("#output").html(the_id);
});
});
I have made an Example for you > JS Fiddle
Hope you can use the jQuery Code.

Recreate Select Control Options Dynamically jQuery

I have the following function which I use to populate a Select control with options. I am grabbing values from objects on the document, and if a condition is met, throwing another value into a Select Control as an option...
function dispatchList() {
//grab list element
var list = document.getElementById("techName");
//foreach div assigned the .square class,
$('.square').each(function () {
//convert each div with .square class toString
var square = $(this).html().toString();
//grab availability value
var availability = $(this).find('tr:eq(4)').find('td').text();
//grab IP
var online = $(this).find('tr:eq(3)').find('td').text()
//if availability and IP values meet below condition...
if ((availability === "True") && (online.indexOf("10.") === 0)) {
//grab the name value from this div
var availableName = $(this).find('tr:eq(0)').find('td').text();
//create a new option element
var item = document.createElement("option");
//create a new text node containing the name of the tech
item.appendChild(document.createTextNode(availableName));
//append the new text node (option) to our select control
list.appendChild(item);
}
})
}
This function works great, but it runs when the document is ready. I need it to run when the document is ready, but also to recreate this list without refreshing the page. Ideally the select control could be emptied and recreated with a click event on a div.
This is the part I have struggled with. I have the following click event which it would make sense to chain this to, but I have not been able to work it out...
function availability() {
//for each element with a class of .square...
$('.square').each(function () {
//grab the id of each input element (button) contained in each .square div...
var btnId = $(this).find("input").attr("id");
//when .square div is clicked, also click it's associated asp button...
$(this).on('click', function (clickEvent) {
document.getElementById(btnId).click();
//****AND ALSO TRIGGER THE dispatchList() FUNCTION TO REBUILD THE #techName LIST****
})
})
}
Can this be done without AJAX or some other post back on the select control?
Does the #techName list need to be emptied first, and then rebuilt?
Thank you for any advice!
$(document).ready(function(){
$(".square").on('click', function (clickEvent) {
var el = clickEvent.target || clickEvent.srcElement
document.getElementById($(el).find('input').attr("id")).click();
dispatchList();
})
})
That's all i can do with the given question. I didn't test the code. You can give fiddle or anything to test. Also this function is written in the browser.

JS variable to HTML elements and vice versa

In the beginning I have JSON data, and I need to convert and output the list to html.
var frut =
{
"wtfrut": [
["0x01", "Apple"],
["0x02", "Orange"],
["0x03", "Pineapple"],
["0x04", "Banana"]
],
[other irrelevant elements]
}
I made it an html <select> plus list of <options> . . .
<select>
<option data-index="0x01">Apple</option>
<option data-index="0x02">Orange</option>
<option data-index="0x03">Pineapple</option>
<option data-index="0x04">Banana</option>
</select>
. . . and stuck it in a js variable.
This <select> list is a cell in a table, and needs to appear in a couple hundred rows.
While building the table, when I need to display the dropdown, I need to go back thru and find the selected attribute of each <select><option>
Problem 1)
The best I can get from
var select = document.createElement("select");
var options = document.createElement("option");
options.setAttribute("value", element[1]);
...
select.appendChild(options);
return select;
is [object HTMLSelectElement] where the dropdown was supposed to be. return select.value returns the value attribute of the first item on the list.
Therefore, I have resorted to stuffing var dropDown with raw html.
out += "<option value=\"" + element[1] + "\" data-hex = \"" + element[0] + "\" data-index = \"" + index + "\">";
because it works. dropDown winds up with the <select> and all <option>s. And it works when I call it with
"<td class=\"vkeyName\" data-f4key-index = \"+index+\">" + dropDown + "</td>"
Problem 2)
Now that that's working, I try to take dropDown back to js at render time (during the loop that produces the above <td>) and figure out which <option> needs to be chosen as default for the dropdown. select.length returns the string length which I understand. It's just a js string.
Overall
What I don't understand is how to get data over the threshold between js variable and valid html element, in either direction. To make that js string into a list of valid html elements that can be output to the html page... Or to take valid html elements, put them into a variable to be worked by js.
getElementBy* and document.write doesn't work. I presume because I don't have the document yet, I'm building objects.
At this point I'm uninterested in js libraries and helpers. This is a learning project and I want to understand this so that things like jQuery aren't so magical.
I made a small example of a way how you could do create a combobox that generates an Array of some kind of data, and how you could help out yourself by using some callback functions to get the value and the text, and how to choose which element should be preselected, and how you could react on changes in the html element.
You can always use document.getElementById, but you have to wait until you are sure that the page got loaded, one way to do it, is to wait for the window.onload function to fire (which means that the DOM is ready to be manipulated, scripts and css are loaded)
In vanilla javascript, you can do it by registering a callback function on the load event, like this:
window.addEventListener('load', function() { ... });
To generate the combobox, I made a small helper namespace and added a comboBoxGenerator, that takes an object in, and generates the combobox in your desired targetElement.
I then iterate the data and for each element, get the value and text over a callback function (that you define when you called the generator) and it returns the value and the text for that single option. It also determines if the element should be preselected.
By registering to the change event of the combobox, you can then find out which element was actually selected, and for that I also added a small function that displays that the function got changed
The 'use strict;' statement helps to add for example forEach function to the array, and will help you to keep your code more clean
I also documented the source a bit, so that you hopefully understand what everything is doing :)
'use strict';
var helper = {};
(function(ns) {
function comboBoxGenerator(options) {
// get the element that you are targetting
var el = document.getElementById(options.target),
cmb = document.createElement('select'),
option;
// iterate the data, and for each element in the array, create an option and call the defined callback functions
options.data.forEach(function(item) {
option = document.createElement('option');
option.value = options.valueSelector(item);
option.text = options.textSelector(item);
option.selected = options.isSelected(item);
// add the option to the combobox
cmb.appendChild(option);
});
// listen to changes on the combobox and then call the selectionChanged event
cmb.addEventListener('change', function(e) {
// this = cmb because of the bind statement on below
// call the selectionChanged callback function, and assing the cmb as the this for the callback function (.apply(this, ...))
options.selectionChanged.apply(this, [this.options[this.selectedIndex]]);
}.bind(cmb));
el.appendChild(cmb);
}
// set the combo function on the helper by either reusing an existing function, or the function just written above
ns.combo = ns.combo || comboBoxGenerator;
}(helper));
// wait till all resources are loaded, and then generate the combobox
window.addEventListener('load', function() {
var dummyData = {
"wtfrut": [
["0x01", "Apple"],
["0x02", "Orange"],
["0x03", "Pineapple"],
["0x04", "Banana"]
]
}, selectedValue = "0x03";
// call the helper method with an object defining the data, targetelement, and callback functions
helper.combo({
target: 'myTable',
data: dummyData.wtfrut,
valueSelector: function(item) {
// item would be like ["0x01", "Apple"], return "0x01" for value
return item[0];
},
textSelector: function(item) {
return item[1];
},
isSelected: function(item) {
// check if the item matches a selectedValue if so, return true, not false
return item[0] === selectedValue;
},
selectionChanged: function(item) {
// gets called when the selection is changed, item = Option, value is the current value, this = combobox
selectedValue = item.value;
console.log('selectedValue changed to ' + selectedValue + ' index = ' + this.selectedIndex);
}
});
});
<div>
<div id="myTable">
</div>
</div>

Modify function to filter by several data-attributes simultaneously

The function below allows users to filter products by data-attributes, and accommodates filtering by multiple values simultaneously. It does this by creating an array of the values selected, and when any of the values are clicked (in this case checked/unchecked) it hides all the items and then re-shows those that match the values in the updated array.
It works correctly when filtering for one data-attribute, but when combined to filter by more than one attribute it no longer shows all results matching any of the values and instead only shows results matching all the specified values.
I've posted a fiddle which demonstrates the problem here: http://jsfiddle.net/chayacooper/WZpMh/94/ All but one of the items have the values of both data-style="V-Neck" and data-color="Black" and they should therefore remain visible if either of the filters are selected, but if another value from a different data-attribute some of the items are hidden.
$(document).ready(function () {
var selected = [];
$('#attributes-Colors *').click(function () {
var attrColor = $(this).data('color');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrColor),1);
}
else {
$this.parent().addClass("active");
selected.push(attrColor);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-color *="' + item + '"]').show();
});
return false;
});
$('#attributes-Silhouettes *').click(function () {
var attrStyle = $(this).data('style');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrStyle),1);
}
else {
$this.parent().addClass("active");
selected.push(attrStyle);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-style *="' + item + '"]').show();
});
return false;
});
});
Both of your handlers are updating the selected array, but only one handler executes on a click. The first one if a color was (de)selected, the second if a style. Let's say you've clicked on "Black" and "Crew Neck". At that time your selected array would look like this: [ "Black", "Crew_Neck" ]. The next time you make a selection, let's say you click "Short Sleeves", the second (style) handler executes. Here's what is happening:
Short_Sleeves gets added to the selected array.
All of the items are hidden using $("#content").find("*").hide();
The selected array is iterated and items are shown again based on a dynamic selector.
Number 3 is the problem. In the above example, a style was clicked so the style handler is executing. Any items in the selected array that are colors will fail because, for example, no elements will be found with a selector such as $('#content').find('[data-style *="Black"]').show();.
I would suggest 2 things.
Keep 2 arrays of selections, one for color, one for style.
Combine your code to use only a single handler for both groups.
Here's a (mostly) working example.
Note that I added a data-type="color|style" to your .filterOptions containers to allow for combining to use a single handler and still know which group was changed.
Here's the full script:
$(document).ready(function () {
// use 2 arrays so the combined handler uses correct group
var selected = { color: [], style: [] };
// code was similar enough to combine to 1 handler for both groups
$('.filterOptions').on("click", "a", function (e) {
// figure out which group...
var type = $(e.delegateTarget).data("type");
var $this = $(this);
// ...and the value of the checkbox checked
var attrValue = $this.data(type);
// same as before but using 'type' to access the correct array
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected[type].splice(selected[type].indexOf(attrValue),1);
}
else {
$this.parent().addClass("active");
selected[type].push(attrValue);
}
// also showing all again if no more boxes are checked
if (attrValue == 'All' || $(".active", ".filterOptions").length == 0) {
$('#content').find('*').show();
}
else {
// hide 'em all
$("#content").find("*").hide();
// go through both style and color arrays
for (var key in selected) {
// and show any that have been checked
$.each(selected[key], function(index,item) {
$('#content').find('[data-' + key + ' *="' + item + '"]').show();
});
}
}
});
});
UPDATE: incorporating suggestions from comments
To make the handler work with checkboxes instead of links was a small change to the event binding code. It now uses the change method instead of click and listens for :checkbox elements instead of a:
$('.filterOptions').on("change", ":checkbox", function (e) {
// handler code
});
The "All" options "hiccup" was a little harder to fix than I thought it would be. Here's what I ended up with:
// get a jQuery object with all the options the user selected
var checked = $(":checked", ".filterOptions");
// show all of the available options if...
if (checked.length == 0 // ...no boxes are checked
|| // ...or...
checked.filter(".all").length > 0) // ...at least one "All" box is checked...
{
// remainder of code, including else block, unchanged
}
I also added an all class to the appropriate checkbox elements to simplify the above conditional.
Updated Fiddle

Categories