jQuery Closures, Loops and Events - javascript

I have a question similar to the one here: Event handlers inside a Javascript loop - need a closure? but I'm using jQuery and the solution given seems to fire the event when it's bound rather than on click.
Here's my code:
for(var i in DisplayGlobals.Indicators)
{
var div = d.createElement("div");
div.style.width = "100%";
td.appendChild(div);
for(var j = 0;j<3;j++)
{
var test = j;
if(DisplayGlobals.Indicators[i][j].length > 0)
{
var img = d.createElement("img");
jQuery(img).attr({
src : DisplayGlobals.Indicators[i][j],
alt : i,
className: "IndicatorImage"
}).click(
function(indGroup,indValue){
jQuery(".IndicatorImage").removeClass("active");
_this.Indicator.TrueImage = DisplayGlobals.Indicators[indGroup][indValue];
_this.Indicator.FalseImage = DisplayGlobals.IndicatorsSpecial["BlankSmall"];
jQuery(this).addClass("active");
}(i,j)
);
div.appendChild(img);
}
}
}
I've tried a couple of different ways without success...
The original problem was that _this.Indicator.TrueImage was always the last value because I was using the loop counters rather than parameters to choose the right image.

You're missing a function. The .click function needs a function as a parameter so you need to do this:
.click(
function(indGroup,indValue)
{
return function()
{
jQuery(".IndicatorImage").removeClass("active");
_this.Indicator.TrueImage = DisplayGlobals.Indicators[indGroup][indValue];
_this.Indicator.FalseImage = DisplayGlobals.IndicatorsSpecial["BlankSmall"];
jQuery(this).addClass("active");
}
}(i,j);
);

Solution by Greg is still valid, but you can do it without creating additional closure now, by utilizing eventData parameter of jQuery click method (or bind or any other event-binding method, for that matter).
.click({indGroup: i, indValue : j}, function(event) {
alert(event.data.indGroup);
alert(event.data.indValue);
...
});
Looks much simpler and probably more efficient (one less closure per iteration).
Documentation for bind method has description and some examples on event data.

Nikita's answer works fine as long as you are using jQuery 1.4.3 and later. For versions previous to this (back to 1.0) you will have to use bind as follows:
.bind('click', {indGroup: i, indValue : j}, function(event) {
alert(event.data.indGroup);
alert(event.data.indValue);
...
});
Hope this helps anyone else still using 1.4.2 (like me)

Related

Add event listener to element after DOMContentLoaded

I have some trouble with adding event listener to element after DOM updating.
I have some page, that sort two lists and save the stage.
I can move elements between this lists by d&d and by clicking special button. And it work fine for me.
https://jsfiddle.net/bmj32ma0/2/
But, I have to save stage of this lists, and after reloading I have to extract stage, so I write code below.
function saveFriendsLists(e) {
if(e.target.classList.contains("b--drugofilter--save-button")){
var vkFriends = document.querySelector('.b--friends-from-vk .js--friends-container').innerHTML;
var choosenFriends = document.querySelector('.b--friends-choosen .js--friends-container').innerHTML;
localStorage.setItem('vkFriends', vkFriends);
localStorage.setItem('choosenFriends', choosenFriends);
}
}
function loadFriensListFromStorage() {
if(localStorage&&localStorage.choosenFriends&&localStorage.vkFriends){
document.querySelector('.b--friends-from-vk .js--friends-container').innerHTML = localStorage.vkFriends;
document.querySelector('.b--friends-choosen .js--friends-container').innerHTML = localStorage.choosenFriends;
}
}
document.addEventListener("DOMContentLoaded", loadFriensListFromStorage);
But after adding this, the preview functionality like D&D doesn't work. And I can't provide you valid jsfidle because, as I can understand, localStoradge reason or something.
When I tried to move my addEventListener to loadFriensListFromStorage function, like this:
function loadFriensListFromStorage() {
if(localStorage&&localStorage.choosenFriends&&localStorage.vkFriends){
document.querySelector('.b--friends-from-vk .js--friends-container').innerHTML = localStorage.vkFriends;
document.querySelector('.b--friends-choosen .js--friends-container').innerHTML = localStorage.choosenFriends;
}
[].forEach.call(friends, function(friend) {
friend.addEventListener('dragstart', handleDragStart, false);
});
}
But that doesn't have any effect.
How can I fix this issue? Thx.

jQuery slideDown not working on element with dynamically assigned id

EDIT: I cleaned up the code a bit and narrowed down the problem.
So I'm working on a Wordpress site, and I'm trying to incorporate drop-downs into my menu on mobile, which means I have to use jQuery to assign classes and id's to my already existing elements. I have this code that already works on premade HTML, but fails on dynamically created id's.
Here is the code:
...
var menuCount = 0;
var contentCount = 0;
//find the mobile menu items
var submenus = $('[title="submenu"]');
if (submenus.length && submenus.parent('.fusion-mobile-nav-item')) {
console.log(submenus);
submenus.addClass('dropdown-title').append('<i id="dropdown-angle" class="fa fa-angle-down" aria-hidden="true"></i>');
submenus.each(function() {
$(this).attr("href", "#m" + menuCount++);
})
var content = submenus.parent().find('ul.sub-menu');
content.addClass('dropdown-content');
content.each(function() {
$(this).attr("id", "m" + contentCount++);
})
}
$(document).on('click', '.dropdown-title', function(e) {
var currentAttrValue = $(this).attr('href');
if ($(e.target).is('.d-active') || $(e.target).parent('.dropdown-title').is('.d-active')) {
$(this).removeClass('d-active');
$(currentAttrValue).slideUp(300).removeClass('d-open');
} else {
$('.dropdown-title').removeClass('d-active');
$('.dropdown-content').slideUp(300).removeClass('d-open');
$(this).addClass('d-active');
console.log($(currentAttrValue));
//THIS LINE FAILS
$(currentAttrValue).slideDown(300).addClass('d-open');
}
e.preventDefault();
});
I've registered the elements with the class dropdown-title using $(document).on(...) but I can't figure out what I need to do to register the elements with the custom ID's. I've tried putting the event callback inside the .each functions, I've tried making custom events to trigger, but none of them will get the 2nd to last line of code to trigger. There's no errors in the console, and when I console log the selector I get this:
[ul#m0.sub-menu.dropdown-content, context: document, selector: "#m0"]
0
:
ul#m0.sub-menu.dropdown-content
context
:
document
length
:
1
selector
:
"#m0"
proto
:
Object[0]
So jQuery knows the element is there, I just can't figure out how to register it...or maybe it's something I'm not thinking of, I don't know.
If you are creating your elements dynamically, you should be assigning the .on 'click' after creating those elements. Just declare the 'on click' callback code you posted after adding the ids and classes instead of when the page loads, so it gets attached to the elements with .dropdown-title class.
Check this jsFiddle: https://jsfiddle.net/6zayouxc/
EDIT: Your edited JS code works... There also might be some problem with your HTML or CSS, are you hiding your submenus? Make sure you are not making them transparent.
You're trying to call a function for a attribute, instead of the element. You probably want $(this).slideDown(300).addClass('d-active'); (also then you don't need $(this).addClass('d-active'); before)
Inside submenus.each loop add your callback listener.
As you are adding the class dropdown-title dynamically, it was not available at dom loading time, that is why event listener was not attached with those elemnts.
var menuCount = 0;
var contentCount = 0;
//find the mobile menu items
var submenus = $('[title="submenu"]');
if (submenus.length && submenus.parent('.fusion-mobile-nav-item')) {
console.log(submenus);
submenus.addClass('dropdown-title').append('<i id="dropdown-angle" class="fa fa-angle-down" aria-hidden="true"></i>');
submenus.each(function() {
$(this).attr("href", "#m" + menuCount++);
// add callback here
$(this).click( function(e) {
var currentAttrValue = $(this).attr('href');
if ($(e.target).is('.d-active') || $(e.target).parent('.dropdown-title').is('.d-active')) {
$(this).removeClass('d-active');
$(currentAttrValue).slideUp(300).removeClass('d-open');
} else {
$('.dropdown-title').removeClass('d-active');
$('.dropdown-content').slideUp(300).removeClass('d-open');
$(this).addClass('d-active');
console.log($(currentAttrValue));
$(currentAttrValue).slideDown(300).addClass('d-active');
}
e.preventDefault();
});
})
var content = submenus.parent().find('ul.sub-menu');
content.addClass('dropdown-content');
content.each(function() {
$(this).attr("id", "m" + contentCount++);
})
}
Turns out my problem is that jQuery is adding to both the mobile menu and the desktop menu, where the desktop menu is being loaded first when I search for that ID that's the one that jQuery finds. So it turns out I was completely wrong about my suspicions.

jQuery Animation, Chaining, .each() and .animate() (or fadeIn() and fadeOut())

I'm having a bit of a trouble trying to figure this out today, i want to make 5 items inside my DOM (which is listed under the same attribute element, $('.elements')) fade in and out, and after reading up a bit on the API i thought .each() would be a fabulous idea to implement a fade in and fade out showcase gallery.
However, i'm currently using:
$('.elements').each(function() {
$(this).fadeIn(2000).delay(200).fadeOut(2000).show();
})
but everything gets faded in and out at once.
How do i do a sequential effect where everything is chained together and it starts from the first item in the list (a.k.a - $('elements').eq(0)?) down to the last one, and then restarts again?
Do i really need a while loop to do this in javascript/jquery? I was hoping there would be a similar function that i could chain for jQuery to perform to reduce load and filesize.
Also, is there a way to restrict the images from overflowing out from my div?
(function loop() {
$('.elements').each(function() {
var $self = $(this);
$self.parent().queue(function (n) {
$self.fadeIn(2000).delay(200).fadeOut(2000, n);
});
}).parent().promise().done(loop);
}());
demo: http://jsfiddle.net/uWGVN/2/
updated to have it looping without end.
2nd update: a different, probably more readable, approach:
(function fade(idx) {
var $elements = $('.elements');
$elements.eq(idx).fadeIn(2000).delay(200).fadeOut(2000, function () {
fade(idx + 1 < $elements.length ? idx + 1 : 0);
});
}(0));
​demo: http://jsfiddle.net/uWGVN/3/
You can add a callback
offical doc :
('#clickme').click(function() {
$('#book').fadeOut('slow', function() {
// Animation complete.
});
});
and call the same function with i++ et $('.elements').eq(i)
http://jsfiddle.net/dFnNL/
For your overflowing , style it with CSS:
div.(class) { position:relative; overflow:hidden; }
Beautiful way :
(function hideNext(jq){
jq.eq(0).hide("slow", function(){
(jq=jq.slice(1)).length && hideNext(jq);
});
})($('a'))
last first :
(function hideNext(jq){
jq.eq(jq.length-1).hide("slow", function(){
(jq=jq.slice(0,length-1)).length && hideNext(jq);
});
})($('a'))

how to get outerHTML with jquery in order to have it cross-browser

I found a response in a jquery forum and they made a function to do this but the result is not the same.
Here is an example that I created for an image button:
var buttonField = $('<input type="image" />');
buttonField.attr('id', 'butonFshi' + lastsel);
buttonField.val('Fshi');
buttonField.attr('src', 'images/square-icon.png');
if (disabled)
buttonField.attr("disabled", "disabled");
buttonField.val('Fshi');
if (onblur !== undefined)
buttonField.focusout(function () { onblur(); });
buttonField.mouseover(function () { ndryshoImazhin(1, lastsel.toString()); });
buttonField.mouseout(function () { ndryshoImazhin(0, lastsel.toString()); });
buttonField.click(function () { fshiClicked(lastsel.toString()); });
And I have this situation:
buttonField[0].outerHTML = `<INPUT id=butonFshi1 value=Fshi src="images/square-icon.png" type=image jQuery15205073038169030395="44">`
instead the outer function I found gives buttonField.outer() = <INPUT id=butonFshi1 value=Fshi src="images/square-icon.png" type=image>
The function is:
$.fn.outer = function(val){
if(val){
$(val).insertBefore(this);
$(this).remove();
}
else{ return $("<div>").append($(this).clone()).html(); }
}
so like this I loose the handlers that I inserted.
Is there anyway to get the outerHTML with jquery in order to have it cross-browser without loosing the handlers ?!
You don't need convert it to text first (which is what disconnects it from the handlers, only DOM nodes and other specific JavaScript objects can have events). Just insert the newly created/modified node directly, e.g.
$('#old-button').after(buttonField).remove();`
after returns the previous jQuery collection so the remove gets rid of the existing element, not the new one.
Try this one:
var html_text = `<INPUT id=butonFshi1 value=Fshi src="images/square-icon.png" type=image jQuery15205073038169030395="44">`
buttonField[0].html(html_text);
:)
Check out the jQuery plugin from https://github.com/darlesson/jquery-outerhtml. With this jQuery plugin you can get the outerHTML from the first matched element, replace a set of elements and manipulate the result in a callback function.
Consider the following HTML:
<span>My example</span>
Consider the following call:
var span = $("span").outerHTML();
The variable span is equal <span>My example</span>.
In the link above you can find more example in how to use .outerHTML() plug-in.
This should work fine:
var outer = buttonField.parent().html();

Calling a jQuery method at declaration time, and then onEvent

I've been writing JS (mainly jQuery) for quite a few months now, but today I decided to make my first abstraction as a jQuery method. I already have working code but I feel/know that I'm not doing it the right way, so I come here for some enlightenment.
Note: Please do not reply that there's already something out there that does the trick as I already know that. My interest in this matter is rather educational.
What my code is intended to do (and does):
Limit the characters of a textfield and change the color of the counter when the user is approaching the end.
And here's what I have:
$(function(){
$('#bio textarea').keyup(function(){
$(this).char_length_validation({
maxlength: 500,
warning: 50,
validationSelector: '#bio .note'
})
})
$('#bio textarea').trigger('keyup');
})
jQuery.fn.char_length_validation = function(opts){
chars_left = opts.maxlength - this.val().length;
if(chars_left >= 0){
$(opts.validationSelector + ' .value').text(chars_left);
if(chars_left < opts.warning){
$(opts.validationSelector).addClass('invalid');
}
else{
$(opts.validationSelector).removeClass('invalid');
}
}
else{
this.value = this.value.substring(0, opts.maxlength);
}
}
In the HTML:
<div id="bio">
<textarea>Some text</textarea>
<p class="note>
<span class="value">XX</span>
<span> characters left</span>
</p>
</div>
Particularly I feel really uncomfortable binding the event each on each keyup instead of binding once and calling a method later.
Also, (and hence the title) I need to call the method initially (when the page renders) and then every time the user inputs a character.
Thanks in advance for your time :)
chars_left is a global variable which is not good at all. Here is a better (slightly changed) version:
jQuery.fn.char_length_validation = function(opts) {
this.each(function() {
var chars_left = opts.maxlength - $(this).val().length;
$(this).keyup(function() {
chars_left = opts.maxlength - $(this).val().length;
if (chars_left >= 0) {
$(opts.validationSelector).text(chars_left);
if (chars_left < opts.warning) {
$(opts.validationSelector).addClass('invalid');
}
else {
$(opts.validationSelector).removeClass('invalid');
}
}
else {
$(this).val($(this).val().substring(0, opts.maxlength));
}
});
});
this.keyup(); // makes the "initial" execution
return this;
};
See a DEMO.
Some explanation:
In a jQuery plugin in function, this refers to the elements selected by the selector. You should use this.each() to loop over all of these and set up every element accordingly.
In this example, every element gets its on chars_left variable. The event handler passed to keyup() has access to it as it is a closure. Update: It is already very late here ;) It is not necessary to declare it here as you recompute the value every time anyway. Still, it should give you an idea how to have private variables that persist over time.
You should always return this to support chaining.
Further thoughts:
You might want to think about how you could make it work for several textareas (i.e. you have to think about the validation selector). Don't tie it to a specific structure.
You should have default options.
Update: Of course you can make your plugin work with only one textarea (like some jQuery functions work).
You can do the binding and initial triggering in the method:
jQuery.fn.charLengthValidation = function(opts) {
return this.keyup(function() {
var charsLeft = opts.maxLength - $(this).val().length;
if (charsLeft >= 0) {
$(opts.validationSelector + ' .value').text(charsLeft);
$(opts.validationSelector).toggleClass('invalid', charsLeft < opts.warning);
} else {
$(this).val($(this).val().substring(0, opts.maxLength));
}
}).trigger('keyup');
}
$(function() {
$('#bio textarea').charLengthValidation({
maxLength: 25,
warning: 10,
validationSelector: '#bio .note'
});
});

Categories

Resources