I have this code:
var elementSelector = 'select.myClass';
$(elementSelector).each(function(i){
/*
* if (several conditions return true) {
*/
doSomeDOMChanges(); //Change the DOM and add several more <select class="myClass">
/*
* }
*/
});
Problem is, within that loop, $(elementSelector) is not reevaluated, and its' size remains the same throughout the entire loop. $(elementSelector).length for example would stay 18, even though it actually increased to 20 within the 4th iteration.
My question is, how to force JQuery to re-select $(elementSelector) on the next iteration (without "resetting" the loop / repeating previous iterations, of course).
Thank you.
mark used classes with an attribute or some class, and select which are not used
var elementSelector = $('select.myClass:not[data-used=1]');
$(elementSelector).each(function(i){
$(this).data('used', 1)
if (several condition return true) {
doSomeDOMChanges(); //Change the DOM and add several more <select class="myClass">
}
});
If it is guaranteed that the modified DOM content occurs further in the document than the element that you are currently visiting in the loop, then you could rely on DOM methods that return a live NodeList, such as:
getElementsByTagName
getElementsByClassName
getElementsByName
children
Here is a demo where the DOM is extended with one more select element, when the value of the current select element has less than 5 characters. The newly inserted select element will have options which have values that are 1 character longer.
function doSomeDOMChanges(select) {
$(select).after($("<select>").addClass("myClass").append(
$("<option>").text($(select).val() + "0"),
$("<option>").text($(select).val() + "1"),
$("<option>").text($(select).val() + "2")
));
}
for (var select of document.getElementsByTagName("select")) {
if ($(select).is(".myClass")) {
if ($(select).val().length < 5) {
doSomeDOMChanges(select);
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<select class="myClass">
<option>2
<option selected>3
<option>5
</select>
The downsides:
You cannot get such a live collection from a call to $(), using the power of a CSS selector
It only works when the DOM changes are further in the document. If changes are made in content that is already "behind" the currently iterated element, the loop will not work as expected, as the new DOM element is inserted in the live collection before the currently iterated element, meaning it will not be visited, and maybe even worse: the current element will move to a higher index in that collection and be visited again by the loop.
If one of these points is a problem, you could just make the jQuery selection again, and keep a Set of the elements you had already visited. The next demo makes DOM changes before the currently visited element:
function doSomeDOMChanges(select) {
$(select).before($("<select>").addClass("myClass").append(
$("<option>").text($(select).val() + "0"),
$("<option>").text($(select).val() + "1"),
$("<option>").text($(select).val() + "2")
));
}
let visited = new WeakSet;
let selector = "select.myClass";
for (let more = true; more; null) {
more = false;
$(selector).each(function() {
if (visited.has(this)) return;
visited.add(this);
if ($(this).val().length < 5) {
doSomeDOMChanges(this);
more = true;
}
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<select class="myClass">
<option>2
<option selected>3
<option>5
</select>
Related
The Problem:
Hey everyone. I'm trying to create a simple function that identifies the next and previous elements of a current item within the ".length" of elements in a div, and then changes the ID of those two elements. Everything is working except for the part where it tries to identify the previous element at the beginning and the next element at the end.
What I've Tried:
It used to be that it would identify those items by using ".nextElementSibling" and ".previousElementSibling", but I realized that since it starts at the first element within the div then it would begin leaking out and trying to identify the previous element outside of the div. I decided to use a for loop that creates a list of the elements with the specific class name, which works as intended. It begins to run into issues again, though, when it reaches the beginning or the end of the list. I assumed that "[i - 1]" would automatically bring it to the last element if the current was the one at the beginning of the list, and that "[i + 1]" would automatically bring it to the first element if the current was the one at the end of the list. It seems that is not the case.
Is anyone able to help me figure this out? It would be much appreciated.
Note: For the sake of simplicity, I didn't include the JavaScript code that makes it switch between items within the div. That part is fully functional so I don't believe it should affect the underlying concept of this problem.
My Code:
HTML:
<div class="items">
<div id="current-item" class="current-div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
</div>
Javascript:
var divItems = document.getElementsByClassName('div-item'); // Gets number of elements with the specific class.
for (i = 0; i < divItems.length; i++) { // Creates list of elements with the specific class.
if (divItems[i].classList.contains('current-div-item')) { // If it is the current item, then do this:
var next = divItems[i + 1] // Find the next element in the list
var previous = divItems[i - 1] // Find the previous element in the list
next.id = 'next-item' // Change the next element's ID to "next-item"
previous.id = 'previous-item' // Change the previous element's ID to "previous-item"
}
}
You are wanting the items to wrap around that isn't going to happen. For the first item the previous item will be index -1 and for the last item the next index will be 1 larger than the actual number of items in the array.
If you add in a ternary you can get the values to wrap.
var prevIndex = (i === 0) ? divItems.length - 1 : i - 1;
var nextIndex = (i === divItems.length - 1) ? 0 : i + 1;
var next = divItems[prevIndex] // Find the next element in the list
var previous = divItems[nextIndex] // Find the previous element in the list
Based on your HTML code, in logic JS to fetch the all the items based in class it would not fetch the current-div-item as you have written logic to fetch only div-item. So I assume that you also need to change the HTML code. As per my understanding about your requirement I have done some changes and uploading the modified code. Which is working as per you requirement.
HTML:
<div id="current-div-item" class="div-item">Current</div>
<div id="item" class="div-item">Div1</div>
<div id="item" class="div-item">Div2</div>
<div id="item" class="div-item">Div3</div>
<div id="item" class="div-item">Div4</div>
Java Script:
var divItems = document.getElementsByClassName('div-item'); // Gets number of elements with the specific class.
for (i = 0; i < divItems.length; i++) {
if (divItems[i].id=='current-div-item') {
var next;
if (i == divItems.length-1)
next = divItems[0];
else
next = divItems[i + 1];
var previous;
if (i == 0)
previous=divItems[divItems.length-1];
else
previous = divItems[i - 1] // Find the previous element in the list
next.id = 'next-item' // Change the next element's ID to "next-item"
previous.id = 'previous-item' // Change the previous element's ID to "previous-item"
}
}
Attached the screenshot of the modified elements id for your reference
I have a JS for loop that iterates over all elements with a specific class, and then removes the class. However, whilst the loop works for the first element found, it then stops. I cannot see any errors, I've tried it inside a try/catch, and can't see anything else that might be causing the problem. Does anyone have any suggestions? Thanks :)
let visibleTags = document.getElementsByClassName('show');
console.log(visibleTags.length) // length is 2
for (let index = 0; index < visibleTags.length; index++) {
console.log(index); // 0
visibleTags[index].classList.remove('show'); // removes 'show' from element 0
}
// element 1 still has the 'show' class and was not touched by the loop... ?
visibleTags is a "live" DOM query - the elements within it will change as the DOM changes.
Therefore, when you remove the show class from an element, it simultaneously disappears from visibleTags, since your query was for elements with the show class. Thus, as soon as you remove the class, visibleTags.length drops to 1, and your loop will exit because the loop counter is already at 1.
There's a number of ways to work with this:
One solution to this is to run the loop backwards, so that it starts at visibleTags.length and counts back to zero. This way, you can remove the elements and the length will drop, but you'll then move onto the previous one and the loop carries on.
Another option is to run the loop as a while loop and just keep removing the first item: ie:
while (visibleTags.length) {
visibleTags[0].classList.remove('show');
}
This would be my preferred solution.
Finally, you may opt to create a non-live array of the elements that you can loop through. You probably don't need to do this, but it may be a useful option if you need to loop through the same list of elements again later on (eg maybe to restore the show class).
You shouldn't use indexes, visibleTag is a live collection and you're modifying part of the selection criteria (the show class) so the collection itself will change. Since you want to remove show from everything that has the show class, using a while loop like this is better:
let shown = document.getElementsByClassName('show');
while(shown.length > 0) {
shown[0].classList.remove('show');
}
<div>
<div class="show">1</div>
<div class="show">2</div>
<div class="show">3</div>
<div class="show">4</div>
</div>
This is because document.getElementsByClassName() is referencing the actual array of elements matching your class.
So when iterating and changing its class, the element itself does not belongs anymore to the array, thus the index becomes index-1.
A workaround, if you haven't another path to reach the object, is to rely on another class/selector to retrieve the list of elements:
let visibleTags = document.getElementsByClassName('test');
console.log(visibleTags.length) // length is 2
for (let index = 0; index < visibleTags.length; index++) {
console.log(index); // 0
visibleTags[index].classList.remove('show'); // removes 'show' from element 0
}
.test {
width: 300px;
height: 300px;
border: 1px solid #ccc;
}
.show {
background-color: red;
}
<div>
<div class="show test">1</div>
<div class="show test">2</div>
</div>
Try to use this function:
function removeClassFromElements(className) {
document
.querySelectorAll(`.${className}`)
.forEach(el => el.classList.remove(className));
}
For your case:
removeClassFromElements('show');
You could use querySelectorAll to select all the element with class show.
The Document method querySelectorAll() returns a static (not live) NodeList representing a list of the document's elements that match the specified group of selectors. Read more about this selector here
function removeClass() {
let visibleTags = document.querySelectorAll(".show");
console.log("Number of selected Elements: ", visibleTags.length); // length is 2
for (let index = 0; index < visibleTags.length; index++) {
console.log("Index: ", index); // 0
visibleTags[index].classList.remove("show"); // removes 'show' from element 0
}
}
.show {
background-color: red;
}
<button onclick="removeClass()">Remove Class</button>
<br/>
<br/>
<div>
<div class="show test">1</div>
<div class="show test">2</div>
</div>
I want to replace a specific div element with a different one, when it has reached 3 clicks on it. That is the only task, I am trying to accomplish with the code.
I have tried looking at some code that does this but all of them replace it with get go, they don't give you a number amount to specify when to replace it with.
Example: <div id="1"></div> has been clicked on 3 times by a user. Once it exceeds that amount replace it with <div id="3"></div>
Changing the id attribute is not a good idea, instead you can use data- attribute like the following way:
var count = 0; // Declare a variable as counter
$('#1').click(function(){
count++; // Increment the couter by 1 in each click
if(count == 3) // Check the counter
$(this).data('id', '3'); // Set the data attribute
console.log($(this).data('id'));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="1" data-id="1">Click</div>
You could write a JavaScript function that keeps track how often you clicked on a specific DOM element (i. e. the div element with id="1"). As soon as the element was clicked three times, it will be replaced by another DOM element which can be created in JavaScript as well.
var clicks = 0;
function trackClick(el) {
clicks++;
if(clicks === 3) {
var newEl = document.createElement('div');
newEl.textContent = 'Div3';
newEl.id = '3';
el.parentNode.replaceChild(newEl, el);
}
}
<div id="1" onclick="trackClick(this)">Div1</div>
In case you should use a library like jQuery or have another HTML structure, please specify your question to improve this code snippet so that it fits for your purpose.
The main idea is to start listening click events on the first div and count them.
The below code shows this concept. Firstly we put first div into variable to be able to create event listeners on it and also create count variable with initial value: 0. Then pre-make the second div, which will replace the first one later.
And the last part is also obvious: put event listener on a div1 which will increment count and check if it is equal 3 each time click happens.
const div1 = document.querySelector('#id-1');
let count = 0;
// pre-made second div for future replacement
const divToReplace = document.createElement('div');
divToReplace.id = 'id-2';
divToReplace.innerText = 'div 2';
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
div1.parentNode.replaceChild(divToReplace, div1);
}
});
<div id="id-1"> div 1 </div>
Note that this approach is easy to understand, but the code itself is not the best, especially if you will need to reuse that logic. The below example is a bit more complicated - we create a function which takes 2 arguments: one for element to track and another - the element to replace with. Such approach will allow us to reuse functionality if needed.
function replaceAfter3Clicks(elem, newElem) {
let count = 0;
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
elem.parentNode.replaceChild(newElem, elem);
}
});
}
const div1 = document.querySelector('#id-1');
// pre-made second div for future replacement
const div2 = document.createElement('div');
div2.id = 'id-2';
div2.innerText = 'div 2';
replaceAfter3Clicks(div1, div2);
<div id="id-1"> div 1 </div>
If you know, how to use JQuery, just put a click event handler on your div 1. On that handler, increment a click counter to 3. If it reaches 3, replace the div with JQuery again.
If there are multiple divs to replace, use an array of counters instead of a single one, or modify a user-specific data attribute via JQuery.
Using native JavaScript, rather than relying upon library (for all the benefits that might offer), the following approach is possible:
// A named function to handle the 'click' event on the relevant elements;
// the EventObject is passed in, automatically, from EventTarget.addEventListener():
const replaceOn = (event) => {
// caching the element that was clicked (because I'm using an Arrow function
// syntax we can't use 'this' to get the clicked element):
let el = event.target,
// creating a new <div> element:
newNode = document.createElement('div'),
// retrieving the current number of clicks set on the element, after this
// number becomes zero we replace the element. Here we use parseInt() to
// convert the string representation of the number into a base-10 number:
current = parseInt(el.dataset.replaceOn, 10);
// here we update the current number with the decremented number (we use the
// '--' operator to reduce the number by one) and then we update the
// data-replace-on attribute value with the new number:
el.dataset.replaceOn = --current;
// here we discover if that number is now zero:
if (current === 0) {
// if so, we write some content to the created <div> element:
newNode.textContent = "Original element has been replaced.";
// and here we use Element.replaceWith() to replace the current
// 'el' element with the new newNode element:
el.replaceWith(newNode);
}
};
// here we use the [data-replace-on] attribute-selector to search
// through the document for all elements with that attribute, and
// use NodeList.forEach() to iterate over that NodeList:
document.querySelectorAll('[data-replace-on]').forEach(
// using an Arrow function we pass a reference to the current
// Node of the NodeList to the function, and here we use
// EventTarget.addEventListener() to bind the replaceOn function
// (note the deliberate lack of parentheses) to handle the
// 'click' event:
(element) => element.addEventListener('click', replaceOn)
);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
div {
border: 1px solid #000;
display: inline-block;
padding: 0.5em;
border-radius: 1em;
}
div[data-replace-on] {
cursor: pointer;
}
div[data-replace-on]::before {
content: attr(data-replace-on);
}
<div data-replace-on="3"></div>
<div data-replace-on="13"></div>
<div data-replace-on="1"></div>
<div data-replace-on="21"></div>
<div data-replace-on="1"></div>
<div data-replace-on="6"></div>
<div data-replace-on="4"></div>
References:
CSS:
Attribute-selectors ([attribute=attribute-value]).
JavaScript:
Arrow function syntax.
ChildNode.replaceWith().
document.querySelectorAll().
EventTarget.addEventListener().
NodeList.prototype.forEach().
I have a chrome extension that when I click it, it will click all the same element buttons on a page and then after the function is done, I want it to select all the 'P's on the drop down. I have multiple 'pf' classes on the page and will need to set all of the to P. The first function is working, when it gets to the second function, there is an error showing the option.length is undefined. My question is how do it get all the option counts inside a class?
function clickUpdate(_callback) {
var updateArray = document.getElementsByClassName("updateButton");
[].slice.call(updateArray).forEach(function(item) {
item.click();
});
console.log("this is the array legnth: " + updateArray.length);
_callback();
}
//Get select object
var objSelect = document.getElementsByClassName("pf");
//Set selected
setSelectedValue(objSelect, "P");
function setSelectedValue(selectObj, valueToSet) {
clickUpdate(function(){
console.log("I am done with the first function");
});
for (var i = 0; i < selectObj.options.length; i++) {
if (selectObj.options[i].text == valueToSet) {
selectObj.options[i].selected = true;
return;
}
}
}
<select class="pf">
<option selected="selected" value="">Not Run</option>
<option value="P">Pass</option>
<option value="F">Fail</option>
<option value="N">N/A</option></select>
Here's your problem:
//Get select object
var objSelect = document.getElementsByClassName("pf");
This doesn't just return the select dropdown - it returns an HTMLCollection (ie, a group of elements) with any elements matching that class name. Even if there's only one present, it'll come back as a collection. But your setSelectedValue function isn't expecting a collection, just a single element - that's why you get the undefined error.
There are a few ways you can handle this, depending on what you're doing elsewhere on the page:
You can use document.querySelector('.pf') to return the first element with that class - do this if you're only going to have one .pf on the page, or only want to manage one of them.
Alternately, you can use document.getElementsByClassName('pf')[0] to achieve the same thing.
Or you can give the select an id and use document.querySelector('#pf') or document.getElementById('pf')
If, on the other hand, you plan to have multiple .pf elements that all work this way...you'll have to do some refactoring first. But that's a whole other discussion.
I have a form UI whereby several sections require duplicate HTML select list to be updated dynamically from a single, dynamically-updatable select list.
The dynamically-updatable list works just fine, in that new options can be added and removed on-the-fly. I can then get this update to propagate through each of the duplicate lists using JQuery .find(). I have even added a bit of logic to maintain the currently selected index of the original select list.
What I'm not able to do is maintain the selected state of each of the duplicate select lists as new options are added and removed from the original select list. As each update to the original select list iterates through each duplicate select list, they lose their currently selected option index.
Here is an example of my conundrum--*EDIT--I would encourage you to try and execute the code I've provided below and apply your theories before suggesting a solution, as none of the suggestions so far have worked. I believe you will find this problem a good deal trickier than you might assume at first:
<form>
<div id="duplicates">
<!--// I need for each of these duplicates to maintain their currently selected option index as the original updates dynamically //-->
<select>
</select>
<select>
</select>
<select>
</select>
</div>
<div>
<input type="button" value="add/copy" onclick="var original_select = document.getElementById('original'); var new_option = document.createElement('option'); new_option.text = 'Option #' + original_select.length; new_option.value = new_option.text; document.getElementById('original').add(new_option); original_select.options[original_select.options.length-1].selected = 'selected'; updateDuplicates();" />
<input type="button" value="remove" onclick="var original_select = document.getElementById('original'); var current_selected = original_select.selectedIndex; original_select.remove(original_select[current_selected]); if(original_select.options.length){original_select.options[current_selected < original_select.options.length?current_selected:current_selected - 1].selected = 'selected';} updateDuplicates();" />
<select id="original">
</select>
</div>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
function updateDuplicates(){
$("#duplicates").find("select").html($("#original").html());
}
</script>
</form>
It is important to note that the duplicate HTML select lists should remain somewhat arbitrary, if at all possible (i.e.; no ID's) as this method needs to apply generically to other dynamically-created select lists throughout the document.
Thanks in advance!
Still not 100% sure what you're asking but it seems like this should do what you're looking for and is a few less lines of code.
(function () {
function updateDuplicates() {
$("#duplicates").find("select").html($("#original").html());
$('#duplicates select').each(function () {
var lastSelectedValue = $(this).data('lastSelectedValue');
$(this).val(lastSelectedValue || $(this).val());
});
}
$(document).ready(function () {
$('button:contains(remove)').bind('click', function (e) {
e.preventDefault();
var original_select = document.getElementById('original'),
current_selected = original_select.selectedIndex;
original_select.remove(original_select[current_selected]);
if (original_select.options.length) {
original_select.options[current_selected < original_select.options.length ? current_selected : current_selected - 1].selected = 'selected';
}
updateDuplicates();
});
$('button:contains(add/copy)').bind('click', function (e) {
e.preventDefault();
var original_select = document.getElementById('original'),
new_option = document.createElement('option');
new_option.text = 'Option #' + original_select.length;
new_option.value = new_option.text;
document.getElementById('original').add(new_option);
original_select.options[original_select.options.length - 1].selected = 'selected';
updateDuplicates();
});
$('#duplicates select').bind('change', function () {
$(this).data('lastSelectedValue', $(this).val());
});
} ());
} ());
EDIT: I changed your markup to be
<button>add/copy</button>
<button>remove</button>
just set the currently selected item/value of select to some variable, then do your operation,
finally reselect the value to the select.
Okay, I think I have a workable approach to a solution, if not a clumsy one. The tricky part isn't adding a value to the original list, because the added option is always at the end of the list. The problem comes in removing a select option because doing so changes the index of the currently selectedIndex. I've tested using Google Chrome on a Mac with no errors. I have commented the code to demonstrate how I approached my solution:
<form>
<div id="duplicates">
<!--// Each of these select lists should maintain their currently selected index //-->
<select>
</select>
<select>
</select>
<select>
</select>
</div>
<div>
<!--// Using a generic function to capture each event //-->
<input type="button" value="add/copy" onClick="updateDuplicates('add');" />
<input type="button" value="remove" onClick="updateDuplicates('remove');" />
<select id="original">
</select>
</div>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
function updateDuplicates(editMode){
///* Capture the selectedIndex of each select list and store that value in an Array *///
var original_select = document.getElementById('original');
var current_selected = new Array();
$("#duplicates").find("select").each(function(index, element) {
current_selected[index] = element.selectedIndex;
});
switch(editMode){
case "add":
var new_option = document.createElement('option');
new_option.text = 'Option #' + original_select.length;
new_option.value = new_option.text;
original_select.add(new_option);
original_select.options[original_select.options.length-1].selected = 'selected';
///* Traverse each select element and copy the original into it, then set the defaultSelected attribute for each *///
$("#duplicates").find("select").each(function(index, element){
$(element).html($("#original").html());
///* Retrieve the currently selected state stored in the array from before, making sure it is a non -1 value, then set the defaultSelected attribute of the currently indexed element... *///
if(current_selected[index] > -1){
element.options[current_selected[index]].defaultSelected = true;
}
});
break;
case "remove":
var current_index = original_select.selectedIndex;
original_select.remove(original_select[current_index]);
///* Thou shalt not remove from thine empty list *///
if(original_select.options.length){
original_select.options[current_index > 0?current_index - 1:0].selected = 'selected';
}
///* Traverse each select element and copy the original into it... *///
$("#duplicates").find("select").each(function(index, element){
$(element).html($("#original").html());
///* Avoid operating on empty lists... *///
if(original_select.options.length){
///* Retrieve the currently selected state stored in the array from before, making sure it is a non -1 value... *///
if(current_selected[index] > -1){
///* If the stored index state is less or equal to the currently selected index of the original... *///
if(current_selected[index] <= current_index){
element.options[current_selected[index]].defaultSelected = true;
///* ...otherwise, the stored index state must be greater than the currently selected index of the original, and therefore we want to select the index after the stored state *///
}else{
element.options[current_selected[index] - 1].defaultSelected = true;
}
}
}
});
}
}
</script>
</form>
There is plenty of room to modify my code so that options can be inserted after the currently selectedIndex rather than appended to the end of the original select list. Theoretically, a multi-select list/menu should work as well. Have at thee.
I'm sure one of the geniuses here will be able to do this same thing with cleaner, prettier code than mine. Thanks to everyone who reviewed and commented on my original question! Cheers.
If you can reset a little, I think that the problem is you are setting your select list's HTML to another list's HTML. The browser probably doesn't try to preserve the currently selected item if all the of underlying html is being changed.
So, I think what you might try doing is explicitly adding the option elements to the target lists.
Try this jsfiddle. If you select an item other than the default first item and click "add", notice that the selected item is maintained. So you need to be a little more surgical in your managing of the target list items.
Maybe that'll help or maybe I missed the point.