Dynamic event attachment not working correctly [duplicate] - javascript

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
var smartActionsId = ['smartActions1','smartActions5','smartActions10'];
for (var i in smartActionsId) {
console.log("smartActionsId ="+smartActionsId[i]);
$('#' + smartActionsId[i] + ' select').change(function () {
var value = $(this).val();
var disableValue;
var ruleIndex = smartActionsId[i].substr(11);
console.log("smartActionsId ="+smartActionsId[i]+" i ="+i);
if (value === '0') {
disableValue = true;
onRuleToggle(disableValue, ruleIndex)
}
else if (value === '1') {
disableValue = false;
onRuleToggle(disableValue, ruleIndex)
}
});
}
I'm creating change event dynamically for a multiple switch slider items using the above JavaScript code. But problem I'm facing is, when I click on any switch 'i' value gets replaced with the last value i.e. in smartActionsId I have 3 elements, which ever switch I change it effects for last switch (smartActions10).
Could you please help me resolving this issue?

Other answers here fixed your problem, but I think you can refactor your code a little and make it much more understandable.
First, I don't like IDs. in your scenario, you have multiple ids which needs to be treated the same. Why not use one mighty class?
Also, ruleIndex calculated from element's ID? smells rotten.
If it tells you something about the element, it should be in an attribute or a data-* attribute.
The first bit of code fixes the markup by adding ruleIndex as data attribute and adding a .smart-actionable class. (Maybe you can even move this part to the server-side, to provide yourself with easier markup for JS).
Now, this makes the event handling quite simple.
var smartActionsId = ['smartActions1','smartActions5','smartActions10'];
for (var i in smartActionsId) {
$('#' + smartActionsId[i])
.data('ruleIndex', smartActionsId[i].substr(11))
.addClass('smart-actionable');
}
$('.smart-actionable').on('change', 'select', function() {
var value = $(this).val();
var disableValue = (value === '0');
onRuleToggle(disableValue, $(this).data('ruleIndex'));
});
Hope it will help.

You don't want to attach event listeners inside a for loop because the variable that tracks the index is used by each loop cycle. If you do that, the i variable will always equal the length of the array minus 1. Use Array.prototype.forEach() instead to prevent that.
var smartActionsId = ['smartActions1','smartActions5','smartActions10'];
smartActionsId.forEach(function (identifier, index) {
console.log("smartActionsId ="+identifier);
$('#' + smartActionsId[index] + ' select').change(function () {
var value = $(this).val();
var disableValue;
var ruleIndex = smartActionsId[index].substr(11);
console.log("smartActionsId ="+smartActionsId[index]+" index ="+index);
if (value === '0') {
disableValue = true;
onRuleToggle(disableValue, ruleIndex)
}
else if (value === '1') {
disableValue = false;
onRuleToggle(disableValue, ruleIndex)
}
});
});
Please Note: IE8 and down does NOT support Array.prototype.forEach().

You cant use for...in in this case. Please try the code below:
var smartActionsId = ['smartActions1', 'smartActions5', 'smartActions10'];
for (var i = 0; i < smartActionsId.length; i++) {
console.log("smartActionsId =" + smartActionsId[i]);
$('#' + smartActionsId[i] + ' select').change(function() {
var value = $(this).val();
var disableValue;
var ruleIndex = smartActionsId[i].substr(11);
console.log("smartActionsId =" + smartActionsId[i] + " i =" + i);
if (value === '0') {
disableValue = true;
onRuleToggle(disableValue, ruleIndex)
} else if (value === '1') {
disableValue = false;
onRuleToggle(disableValue, ruleIndex)
}
});
}

I've always use names like smartActions_1. If you can use it, then in your .change function you can use
// if 'smartActionsId' is global variable
// and if you need to get position in 'smartActionsId' array
var numInArray = $.inArray( this.parentNode.id, smartActionsId );
// this - your select DOM object
var ruleIndex = parseInt( this.parentNode.id.split( "_" )[ 1 ] );
And remember that this in .change function its select which have no id and you must use this.parentNode or $( this ).parent() to get it's holder (I think its div or somethink like that).
#Jack in comments is right: select may not be a direct child. Then you can use this code:
var parent = $( this ).closest( "[id^=smartActions]" );
var numInArray = $.inArray( parent.attr( "id" ), smartActionsId );
var ruleIndex = parseInt( parent.attr( "id" ).split( "_" )[ 1 ] );

Related

jQuery to Vanilla JS - querySelector issues

I'm going through some code and working to change all of the jQuery to vanilla JS. However there is one section and I keep getting an error in my console that says either:
TypeError: document.querySelectorAll(...).toggle is not a function pr
TypeError: document.querySelectorAll(...) is null
Below is my code, the top part you can see is where I am trying to change the jquery to vanilla js (I have commented out the jquery) :
console.log(shipmentNumbers);
for (let i = 0; i < shipmentNumbers.length; i += 1) {
let sNumber = shipmentNumbers[i];
function getHistory(event) {
console.log(event);
document.querySelectorAll('#shipment' + sNumber + 'tr.show-history' + sNumber).toggle();
// $('#shipment' + sNumber + ' tr.show-history' + sNumber).toggle();
document.getElementsByClassName('overlay-line' + sNumber).style.display = 'table-row';
// $('.overlay-line' + sNumber).css({
// "display": "table-row"
// });
if (flag == false) {
let shipmentNumber = event.currentTarget.id.replace('status', '');
console.log('shipmentNumber=', shipmentNumber);
callHistoryApi(clientId, shipmentNumber);
$(this).find('.expand' + sNumber).html("▼");
flag = true;
} else {
$(this).find('.expand' + sNumber).html("►");
$('.overlay-line' + sNumber).css({
"display": "none"
});
flag = false;
}
}
Can someone explain why this isn't working, and how I can get it working using vanilla js?
I find that writing these two functions can really help when moving from jQuery to native JS.
function domEach(selector, handler, context) {
return Array.from(document.querySelectorAll(selector), handler, context);
}
// If you get a TypeError "Array.from" is not a function, use the polyfill
// found on MPN.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
This gets you around issues where you relied on implicit loops that jQuery uses.
// Instead of these:
document.querySelectorAll('#shipment' + sNumber + 'tr.show-history' + sNumber).toggle();
document.getElementsByClassName('overlay-line' + sNumber).style.display = 'table-row';
// Use these:
domEach('#shipment' + sNumber + 'tr.show-history' + sNumber, function (tr) {
tr.style.display = tr.style.display === "none"
? ""
: "none";
});
domEach('.overlay-line' + sNumber, function (el) {
el.style.display = 'table-row';
});
For a list of techniques to use instead of the jQuery functions, you can check You Might Not Need jQuery
Edit: more information about the code above
jQuery uses implicit loops. That is, when you do this:
$("#one").addClass("two");
jQuery does this behind the scenes:
var elements = document.querySelectorAll("#one");
var i = 0;
var il = elements.length;
while (i < il) {
elements[i].classList.add("two");
i += 1;
}
This leads to some confusion when going from jQuery to vanilla JavaScript since you have to manually loop over the results of querySelectorAll.
Array.from will loop over an array or array-like structure. querySelectorAll will return a NodeList - this is an array-like structure (it has numerical indicies and a length property). The domEach function allows us to pass a CSS selector to the function and will loop over the results of finding matching elements.
The ? : syntax is called a ternary operator. It's a short-cut for if ... else.
// Ternary operator
tr.style.display = tr.style.display === "none"
? ""
: "none";
// Equivalent if/else statements
if (tr.style.display === "none") {
tr.style.display = "";
} else {
tr.style.display = "none";
}
I hope that helps clarify things.
You must add check in whenever you do this, as .querySelctor/All() is going to return "null" if no elements are found.
var myCollection = document.querySelectorAll("selector");
if (myCollection.length > 0){
Array.prototype.forEach.call(myCollenction, function(element){
if(typeof element.toggle === "function"){
element.toggle();
}
})
}
More or less this will help you achieve your goal. However if you don't have method "toggle" defined on your elements - nothing will happen. :)

Using JavaScript to cycle through elements with numbers in the ID [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
I'm interested in cycling through some div's I have on my page that have numbers in the ID.
This is what I have so far...
var count = 1;
while(count != 12) {
$('#logo-'.concat(count).concat(' > img')).click(function() {
if($('#info-'.concat(count)).hasClass('dropdown-active')) {
$('#info-'.concat(count)).slideUp();
$('#info-'.concat(count)).removeClass('dropdown-active');
} else {
$('#info-'.concat(count)).slideDown();
$('#info-'.concat(count)).addClass('dropdown-active');
}
return false;
});
count++;
}
The count seems to stop working when it reaches the if statement.
So the ID's on the page are logo-1, info-1, logo-2, info-2, etc...
You can do this more cleanly as follows:
while(count != 12) {
$('#logo-' + count + ' > img').click(function() {
var origin = $(this).parent(),
targetId = '#info-' + origin[0].id.substring(5),
target = $(targetId);
if(target.hasClass('dropdown-active')) {
target.slideUp();
target.removeClass('dropdown-active');
} else {
target.slideDown();
target.addClass('dropdown-active');
}
return false;
});
count++;
}
But it would be preferable to give all your logos the same class (say, "logo"), and then you can ditch the while loop:
$('.logo > img').click(function() {
var origin = $(this).parent(),
targetId = '#info-' + origin[0].id.substring(5),
target = $(targetId);
if(target.hasClass('dropdown-active')) {
target.slideUp();
target.removeClass('dropdown-active');
} else {
target.slideDown();
target.addClass('dropdown-active');
}
});
Edit: As Karl-André Gagnon points out in the comments, you could also use $('[id^="logo-"]') as an alternative to giving them a class, and still use the no-while-loop approach.
One alternative to parsing the numbers out of the IDs would be to store the number in a data-num attribute:
<div class='logo' data-num='1'>...</div>
And then instead of that var origin... stuff with the substring method, you would have:
var num = $(this).parent().data('num'),
target = $('#info-' + num);
While JLRishe's answer is preferable, here's a largely academic demonstration of doing it with closures:
var count = 1;
while(count != 12) {
(function(id){
$('#logo-'.concat(id).concat(' > img')).click(function() {
if($('#info-'.concat(id)).hasClass('dropdown-active')) {
$('#info-'.concat(id)).slideUp();
$('#info-'.concat(id)).removeClass('dropdown-active');
} else {
$('#info-'.concat(id)).slideDown();
$('#info-'.concat(id)).addClass('dropdown-active');
}
return false;
});
}(count));
count++;
}
By calling a function and passing the value as a parameter, you create a new scope and thus id will maintain it's value in the click function. I wouldn't necessarily recommend this approach (there are many inefficiencies in your code besides), but hopefully it's an interesting demonstration of closures and scope.

For Loop/Each Loop variable storage and comparison (jQuery or Javascript

I have an each loop with jquery (I wouldn't mind using a for loop if the code works better) and it's cycling through all divs with the class="searchMe". I would like to store the current div in a variable that I can use in the next iteration of the loop to compare a value with the new current div. Some code is removed (all working) to simplify things, but here's my code:
$('.searchMe').each(function(){
var id = $(this);
sortUp += 1,
sortDown -= 1;
if (test) {
id.attr("name", ""+sortUp+"");
}
else {
id.attr("name", ""+sortDown+"");
}
if ( id.attr("name") > lastId.attr("name") ) {
id.insertBefore(lastId);
}
lastId = id; //this doesn't work, but illustrates what I'm trying to do
});
Everything is working properly except the last 3 lines.
is this possible with an each/for loop?
I don't know why sort up is needed when you are comparing backwards alone. you can replace the last three lines with...
if ( parseInt(id.attr("name")) > parseInt(id.prev().attr("name")) ) {
id.insertBefore(id.prev()).remove();
}
you can use index in $.each()
like
$('.searchMe').each(function(index){
var id = $(this);
sortUp += 1,
sortDown -= 1;
if (test) {// don't know what is test, let it is predefined
id.attr("name", sortUp);// no need to add ""
}
else {
id.attr("name", sortDown);
}
if ($('.searchMe').eq(index-1).length && id.attr("name") > $('.searchMe').eq(index-1).attr("name") ) {
id.insertBefore($('.searchMe').eq(index-1));
}
});
Or Alternatively you can define lastid like
var lastId='';// let it be global
$('.searchMe').each(function(index){
var id = $(this);
sortUp += 1,
sortDown -= 1;
if (test) {// don't know what is test, let it is predefined
id.attr("name", sortUp);// no need to add ""
}
else {
id.attr("name", sortDown);
}
if (id.attr("name") > lastId.attr("name") ) {
id.insertBefore(lastId);
}
lastId=id;// assign here the current id
});
Read eq() and $.each()

Searching table rows with jQuery Non-Case sensitive [duplicate]

This question already has answers here:
Is there a case insensitive jQuery :contains selector?
(12 answers)
Closed 9 years ago.
I am trying out this nice little jQuery script for searching tables found here:
Searching table rows with jQuery
It works great, however I don't want it to be case sensitive, for example if a value in my table is Value One, I want to be able to search value one, or VALUE ONE and still get the right outcome.
This is the Jquery that controls it:
<script>
$(document).ready(function(){
$('input[name="search"]').keyup(function(){
var searchterm = $(this).val();
if(searchterm.length > 3) {
var match = $('tr.data-row:contains("' + searchterm + '")');
var nomatch = $('tr.data-row:not(:contains("' + searchterm + '"))');
match.addClass('selected');
nomatch.css("display", "none");
} else {
$('tr.data-row').css("display", "");
$('tr.data-row').removeClass('selected');
}
});
});
</script>
Can anyone help me with making it non case sensitive?
You can turn both strings into lowercase with the toLowerCase() method.
It should look something like this:
$(document).ready(function(){
$('input[name="search"]').keyup(function(){
var searchterm = $(this).val();
searchterm=searchterm.toLowerCase();
if(searchterm.length > 3) {
var match = $('tr.data-row:contains("' + searchterm + '")');
var nomatch = $('tr.data-row:not(:contains("' + searchterm + '"))');
match.addClass('selected');
nomatch.css("display", "none");
} else {
$('tr.data-row').css("display", "");
$('tr.data-row').removeClass('selected');
}
});
});
And you will need to override the jQuery method too! Something that looks like this...
$.expr[":"].contains = $.expr.createPseudo(function(arg) {
return function( elem ) {
return $(elem).text().toLowerCase().indexOf(arg.toLowerCase()) >= 0;
};
});
It is pretty straight forward, all that you're doing is forcing both strings to be lower case before comparing them. That way you skip the different cases issue.
Hope this helps, good luck.
Your best best might be to write your own selector expression like this:
$.expr[":"].containsCI = $.expr.createPseudo(function(arg) {
return function( elem ) {
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
};
});
You can find the solution here : Link , this will override the orginal ":contains" selector
$.expr[":"].contains = $.expr.createPseudo(function(arg) {
return function( elem ) {
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
};
});

Process HTML in javascript

I would like to remove all instances of a .class in an html.
remove class "s"
remove <span class="s">some text</span> in html
Output
remove some text in html
What's the easiest way to do this?
Assuming you want to remove it from just this class. Here's how to keep just the text:
$(".s").each(function(){
$(this).replaceWith($(this).text());
});
Code in action.
And if you want to keep the HTML:
$(".s").each(function(){
$(this).replaceWith($(this).html());
});
And here's that code in action.
Here's a plain JavaScript solution. It might look a bit long at first sight, but it simply does what you want:
function moveElements(root) {
var parent = root.parentNode,
e = root.firstChild;
while (e != null) {
var next = e.nextSibling;
parent.insertBefore(e, root);
e = next;
}
parent.removeChild(root);
}
function removeClass(root, name) {
var e = root.firstChild;
while (e != null) {
var next = e.nextSibling;
if (e.nodeType == 1) { // element node
if (e.className == name)
moveElements(e); // move children outside this element
else
removeClass(e, name); // else recursively scan this element
}
e = next;
}
}
removeClass recursively scans elements looking for the specified class, and if found, calls moveElements, which moves the children outside and removes the original element. To run it through the entire document, removing the class s (from your example), call it like this:
removeClass(document.documentElement, 's');
Here's a working example on JSBin.
replaceNode?
http://www.webreference.com/js/column43/replace.html
The replaceNode method is much more intuitive than the removeNode method. While the removeNode method just removes the specified element and makes its descendents children of their grandfather, the replaceNode method deletes the whole subtree that is rooted at the specified element, and substitutes it with a new element.
var msg = "";
function printChildren() {
childCount = bodyNode.childNodes.length;
msg += "childCount = " + childCount;
for(var i = 0; i < childCount; i++) {
msg += "\nchildNodes["+i+"].nodeName = " + bodyNode.childNodes[i].nodeName;
}
}
printChildren();
msg += "\nReplacing Paragraph 3\n";
var b = document.createTextNode("New Body Page");
var replacedNode = p3Node.replaceNode(b);
msg += "\nreplacedNode.nodeName = " + replacedNode.nodeName;
msg += "\nreplacedNode.childNodes.length = " + replacedNode.childNodes.length;
msg += "\np2Node.nodeName = " + p2Node.nodeName;
printChildren();
alert(msg);
Using jQuery you could do:
$(".s").remove();
http://api.jquery.com/remove/

Categories

Resources