Preventing "too much recursion" error in jQuery - javascript

EDIT**
I have this click event
$('.next-question').click(function () {
$('td').removeClass('highlight-problem');
var r = rndWord;
while (r == rndWord) {
rndWord = Math.floor(Math.random() * (listOfWords.length));
}
$('td[data-word="' + listOfWords[rndWord].name + '"]').addClass('highlight-problem');
$('td[data-word=' + word + ']').removeClass('wrong-letter').removeClass('wrong-word').removeClass('right-letter');
var spellSpace = $('td[data-word=' + listOfWords[rndWord].name + ']').hasClass('right-word');
if (spellSpace) {
$('.next-question').trigger('click');
} else {
$("#hintSound").attr('src', listOfWords[rndWord].audio);
hintSound.play();
$("#hintPic").attr('src', listOfWords[rndWord].pic);
$('#hintPic').show();
$('#hintPicTitle').attr('title', listOfWords[rndWord].hint);
$('#hintPicTitle').show();
}
});
When debug in the console it says too much recursion meaning it is in some sort of endless loop at this point. I think it is because of the trigger("click") event in the if statement, because I seen something similar online.
Basically, I want to say, if given word has the class right-word then move on (hence the trigger), else ...
Is there another way to write it that will not crash?
Here is a fiddle: http://jsfiddle.net/Dxxmh/112/
INSTRUCTION: Click the letters on the right to spell the highlighted area in the grid (The images to help you spell the words are not available in a fiddle so you have to spell them using the console, by looking up the td's)

I would do something like this:
if (spellSpace) {
if(score.right != 4)
$('.next-question').trigger('click');
I see like if(score.right == 4) means the end of game. After it is ended - you have no words (or just have no "right" words, not sure) at all and that is why it never stops. It just triggers click forever instead of stop doing anything and wait for user to click Restart button.
I guess that condition is not enough. Not sure how number of wrong words is counted and handled. But it should be enough to move forward and build correct condition based on your programm logic. Any recursion you start (and you start it with trigger("click")) must have a stop condition.

.trigger('click') will just invoke the listener once more. Did you intend to follow the link only in that circumstance? In that case you could return false in your else scenario.

This isn't a jQuery issue: you're manually triggering the same event from within the handler:
$('.next-question').trigger('click');
Now, this will cause an infinite loop if you're not careful. Best way to fix this is not to invoke the handler by triggering the event a second time, but by calling it using a function name:
$('.next-question').click(function callMe(event)
{
//replace: $('.next-question').trigger('click');
//with this:
if (spellSpace && event)
{//also check if the event has been passed
callMe.apply(this,[]);//don't pass event for recursive call
}
});

Try to use this:
$('.next-question').click(function (event) {
event.preventDefault();
});

Related

How to prevent keyup function from lagging when the user types

I am trying to create a search function in jQuery:
$('input').on('keyup', function(){
var searchTerm = $("input").val().toLowerCase();
$('.item').each(function(){
if ($(this).filter('[data-text *= ' + searchTerm + ']').length > 0 || searchTerm.length < 1) {
$(this).parent().show();
} else {
$(this).parent().hide();
}
});
});
Each time the user types in the input, it gets compared to the data attribute value of .item divs. If the data attribute of that element contains the search query, it gets displayed - otherwise hidden.
This works perfectly in Chrome, however it is really laggy in Safari for some reason when the user is typing.
Is there a way to fix this?
There are about 1400 divs (.item), and the data-text attribute is only around 10-20 characters for each element
Edit, fixed by removing .show() and .hide() - and replacing with native Javascript
Solution
I have face similar issue before, I think you might want to try adding something called "debounce", which basically add a delay before doing any process. In the keyup case, it will wait for the user to stop typing for any set amount of time (let's say 0.5 second) and then do the process (searches or whatever) If you don't use debounce, it will do the search every single time the user trigger the keyup event.
You can search for articles on how to do debounce, I think there's a lot of them. But in essence it uses the setTimeout and clearTimeout function of JS
Here's from the first article I found: https://levelup.gitconnected.com/debounce-in-javascript-improve-your-applications-performance-5b01855e086
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
How to use this function? simple, just add your actual function (the search function) as the first parameter, and the delay (microseconds) in the second parameter, and then use the .call() function (why do this? because the debounce will return a function). So I guess something like this:
$('input').on('keyup', function(){
var searchTerm = $("input").val().toLowerCase();
debounce(function(){
$('.item').each(function(){
if ($(this).filter('[data-text *= ' + searchTerm + ']').length > 0 || searchTerm.length < 1) {
$(this).parent().show();
} else {
$(this).parent().hide();
}
});
}, 500).call();
});
This is how I will do it, because then I can add some stuff outside of the debounce into the keyup event, but you can just put the debounce returned function into a variable and then bind it with the keyup (like in the article), or just straight up put the debounce inside the keyup, like this:
$('input').on('keyup', debounce(function(){
...
},500));
How does it works?
You can read them in the articles, or find answer in StackOverflow, here's what I got Can someone explain the "debounce" function in Javascript
But if I'm using my own words, basically what you first need to understand is setTimeout set a timer before a function is called, and clearTimeout cancel that timer. Now in the debounce you can see that there's a clearTimeout before any setTimeout. So every time the keyup event is triggered it will basically cancel the last timeout set (if any), and then it will set a new timeout. In essence, it will reset the timer to what you set every time the event is triggered.
So for example:
The user want to search "abc"
They type "a" -> the debounce set a timer of 500ms before calling the
actual search of "a"
Before the 500ms is up, the user type "b", so the debounce cancel that "a" search, and search for "ab" instead, while also setting a timer of 500ms before doing it
Before the 500ms is up, the user type "c", so cancel the "ab" search, add a timer of 500ms to search for "abc"
The user stop typing until 500ms is up, now the debounce actually call the search for "abc"
What this results to? The heavy processing for the search is only done once for "abc", you can also put a loader or something to this heavy processing so it looks better for the user
Some quick fixes:
Collate the divs then show/hide in a single statement after the each rather than per iteration.
Changing the DOM is relatively expensive, so doing so in a single statement can greatly increase performance
If this is a table change to divs
Tables need to re-render the whole table on small changes. Fixed cell sizes can help. Not the case in this question, just a general improvement
Use an in-memory filter rather than read the DOM for each item.
Reading the DOM is much slower than in-memory (though in-memory uses more memory of course). For example, filter on .data() rather than [data-] as it will use in-memory. It's possible that this is quick in Chrome as Chrome may be caching the [data- attributes, so may not have an improvement in Chrome
debounce the input event so it only occurs when user has finished typing
Wait until the user has "finished" typing then run the action.
use operator associativity to your advantage
Although an edge case, this line
if ($(this).filter('[data-text *= ' + searchTerm + ']').length > 0 || searchTerm.length < 1)
will run the $(this).filter even when searchTerm.length < 1, change to
if (searchTerm.length < 1 || $(this).filter('[data-text *= ' + searchTerm + ']').length > 0)
Example showing this in action
function a() { console.log("a"); return true; }
function b() { console.log("b"); return true; } // b() doesn't need to exist
if (a() || b()) console.log("c")
consider server-side paging / filtering
substantially reduces the "footprint" on the page, so will be much quicker/more responsive, but with a potentially slightly longer delay retrieving the data. Depending on how it's displayed, 1400 records may be a lot for the user to view in one go (hence your filtering).

Command Buttons Not Responding to JS functions

I am very close to finishing this program but am unable to get past one last hurdle. I want some very simple code to execute when the command buttons are pressed. When the Submit Order button is pressed the following code should run to check that the form is completed.
function validateForm()
{
if ($("tax").value = 0)
{
alert ("You have not selected anything to order");
}
if ($("shipCost").value = 0)
{
alert("You must select a method of shipping");
}
}
And when the reset button is pressed the following code should run.
function initForm()
{
$('date').value = todayTxt();
$('qty1').focus();
}
Unfortunately the buttons are not executing the code which I am trying to execute through the following set of functions.
window.onload = function ()
{
initForm();
todayTxt();
productCosts();
shipExpense();
$('shipping').onchange = calcShipping;
calcShipping();
$("Submit Order").onclick = validateForm();
$("reset").onclick = initForm();
}
I have created a fiddle so you can see the full program: http://jsfiddle.net/KhfQ2/ Any help is greatly appreciated.
You're doing it way wrong.
With if statements, you use == instead of =.
= in A = B means assign value of B to A
== in A == B means A equals B
Read about .ready and use it instead of window.onLoad, it's quite a bad choice when it comes to binding, ie.
$( document ).ready(function() {
//taken from api.jquery.com/ready/
});
If you're using jQuery, use # when refering to ID objects, ie.
$('#tax').val();
On no account should you use spaces when giving any object a unique name or class!
Pay attention to letters. You had ".clisk()" instead of "click()".
Check it out and provide us with fixed code.
It is simple. $("Submit Order") doesn't work, because the button doesn't have this id. You can change this to something like $("btn-submit-order"). Same thing to reset.
Moreover, when you test $("tax").value = 0 I think you mistyped = instead of ==.
Other issues...
I think you mean
if ($("#tax").val() == 0)
Note:
Uses the correct selector #
Uses the jQuery val() function. The jQuery object doesn't have a value property.
Compares to 0 using loose checking, though personally I would write the line as
if (+$("#tax").val() === 0)

Trigger event not working or suspected passing on index

.full-arrow is an arrow that selects the next page. .full-navigation is a navigation bar, quite simply boxes in a line that change colour when you select them. The rest of the function isn't on here but you get the general idea.
When I create a trigger event to the function below the first one, it goes through okay but I'm unsure whether it's not picking up the index() or whether it's just not working at all. Weirdly, it works the first time but I think that's because the same_page variable is declared as 0 in the beginning.
The reason I'm also doubting whether it's the index() not being passed on is because the alert("foo"); isn't coming up.
$(".full-arrow").click(function() {
$(".full-navigation li:eq(" + same_page+1 + ")").trigger("click");
});
$(".full-navigation li").click(function(event) {
//alert("foo");
//alert(same_page);
same_page = $(this).index();
if(same_page == $(this).index()) { return false; }
});
Where are you getting the same_page variable from? Try using parseInt( same_page, 10 )--I have a hunch it's actually a string.

Advice on how to hook into a client-side "word typed" event

So i'm working on something where i want to fire off an event (client-side, JavaScript) in two scenarios:
If the user has finished typing a word, e.g: "stack " (fired, with "stack" as a word)
If 2 seconds has passed. This will handle the "last word". (e.g no space after)
So, to sum up, here's how it would fire as i'm typing:
"Stack " (req #1 fired - "Stack")
"Overflow " (req #1 fired - "Overflow")
" Rocks" (req #3 fired - "Rocks")
Hopefully that makes sense.
So, off the top of my head, i'm thinking i would need to hook into the .keypress() event (i'm using jQuery BTW), and then do some string magic (e.g look for spaces, etc)
Is it that simple? Or is there something out there (e.g an "extended" .keypress()handler) that has already done this?
I would :
Register to the input keyup event $("input").bind("keyup", function(e){ });
Check if the pressed key is the space bar if(e.keyCode == 32)
If it is, get the last word from the input $("body").trigger('input_update', { last-word: false, word: $(this).val()).replace(/[\s-]+$/,'').split(/[\s-]/).pop() });
For the 2s timer, just launch a timer when the page is ready, and reset it each time the keyup event on the input is triggered. Reaching, those 2s, the function executed should be like that : function(){ $("body").trigger('input_update', { last-word: true, word: $(this).val()).replace(/[\s-]+$/,'').split(/[\s-]/).pop() }); }
And before all that, register to your custom event : $("body").bind("input_update", function(e, data){ // do your stuff with the last word });
The code may not be 100% correct since i'm tired, but you got the idea.
I don't know if something like that is done already, but there is an easy way to sort it out, check this fiddle.

How to loop through elements and call onblur handler for certain elements

I have a case where I have a bunch of text boxes and radio buttons on a screen all built dynamically with various DIVs. There are onblur routines for all of the text boxes to validate entry, but depending on the radio button selection, the text box entry could be invalid when it was valid originally. I can't use onblur with the radio buttons because they could go from the radio button into one of the text boxes that was made invalid and create an infinite loop since I'm putting focus into the invalid element. Since each text box has its own special parameters for the onblur calls, I figure the best way to do this is to call the onblur event for the textboxes when the form gets submitted to make sure all entry is still valid with the radio button configuration they have selected. I also need it to stop submitting if one of the onblur events returns false so they can correct the textbox that is wrong. This is what I've written:
for (var intElement = 0; intElement < document.forms[0].elements.length; intElement = intElement + 1)
{
if (document.forms[0].elements[intElement].name.substr(3) == "FactorAmount") // The first 3 characters of the name are a unique identifier for each field
{
if (document.forms[0].elements[intElement].onblur())
{
return false;
break;
}
}
}
return true;
I originally had (!document.forms[0].elements[intElement].onblur()) but the alert messages from the onblur events weren't popping up when I had that. Now the alert messages are popping up, but it's still continuing to loop through elements if it hits an error. I've stepped through this with a debugger both ways, and it appears to be looping just fine, but it's either 1) not stopping and returning false when I need it to or 2) not executing my alert messages to tell the user what the error was. Can someone possibly help? It's probably something stupid I'm doing.
The onblur method that is getting called looks like this:
function f_VerifyRange(tagFactor, reaMin, reaMax, intPrecision, sLOB, sIL, sFactorCode)
{
var tagCreditOrDebit;
var tagIsTotal;
var tagPercentageOrDecimal;
eval("tagCreditOrDebit = document.forms[0]." + tagFactor.name.substr(0,3) + "CreditOrDebitC");
eval("tagIsTotal = document.forms[0]." + tagFactor.name.substr(0,3) + "IsTotal");
eval("tagPercentageOrDecimal = document.forms[0]." + tagFactor.name.substr(0,3) + "PercentageOrDecimal");
if (tagPercentageOrDecimal.value == "P")
{
reaMax = Math.round((reaMax - 1) * 100);
reaMin = Math.round((1 - reaMin) * 100);
if (parseFloat(tagFactor.value) == 0)
{
alert("Please enter a value other than 0 or leave this field blank.");
f_SetFocus(tagFactor);
return false;
}
if (tagIsTotal.value == "True")
{
if (tagCreditOrDebit.checked)
{
if (parseFloat(tagFactor.value) > reaMin)
{
alert("Please enter a value less than or equal to " + reaMin + "% for a credit or " + reaMax + "% for a debit.");
f_SetFocus(tagFactor);
return false;
}
}
else
{
if (parseFloat(tagFactor.value) > reaMax)
{
alert("Please enter a value less than or equal to " + reaMin + "% for a credit or " + reaMax + "% for a debit.");
f_SetFocus(tagFactor);
return false;
}
}
}
}
return true;
}
EDIT: I think I've figured out why this isn't working as expected, but I still don't know how I can accomplish what I need to. The line below:
if (!document.forms[0].elements[intElement].onblur())
or
if (document.forms[0].elements[intElement].onblur())
is not returning what the single onblur function (f_VerifyRange) is returning. Instead it is always returning either true or false no matter what. In the first case, it returns true and then quits and aborts the submit after the first textbox even though there was no error with the first textbox. In the second case, it returns false and runs through all the boxes. Even though there might have been errors (which it displays), it doesn't think there are any errors, so it continues on with the submit. I guess what I really need is how to get the return value from f_VerifyRange which is my onblur function.
This question is a bit too involved for me at this time of the night, but I will give you this bit of advice:
eval("tagCreditOrDebit = document.forms[0]." + tagFactor.name.substr(0,3) + "CreditOrDebitC");
This can be written in a MUCH better way:
tagCreditOrDebit = document.forms[0][tagFactor.name.substr(0,3) + "CreditOrDebitC"];
In javascript, anywhere where you can use dotted syntax, you can use square brackets.
document.body;
document['body'];
var b = 'body';
document[b];
Also, think about giving your forms some sort of identifier. I have no clue at all why document.forms[0] was the standard way to address a form for so long... if you decide to place another form on the page before this one, then everything will break!
Other ways to do it include:
// HTML
<form name="myFormName">
// Javascript
var f = document.myFormName;
or
<form id="myFormId">
var f = document.getElementById("myFormId")
You´re not getting any success with if (!...onblur()) because the return of onblur() is always undefined when used directly. OnBlur() is a Event Handler Function. Like you descovered, you have to create a workaround.
I ended up solving this with a global variable. I originally set a value g_bHardEditsPassed to true assuming we will have no errors. Then in f_VerifyRange, everytime I return a value, I put a line before it to set the g_bHardEditsPassed variable to match. Then I modified the loop to look like this...
g_bHardEditsPassed = true;
for (var intElement = 0; intElement < document.forms[0].elements.length; intElement = intElement + 1)
{
if (document.forms[0].elements[intElement].name.substr(3) == "FactorAmount")
{
document.forms[0].elements[intElement].onblur()
if (!g_bHardEditsPassed)
{
g_bHardEditsPassed = true;
return false;
}
}
}
return true;
Thanks for everyone's suggestions. I'm sure that the jQuery thing especially will be worth looking into for the future.
First, for the love of god and all that is holy, stop writing native javascript and help yourself to some of that jQuery :)
Second, start using a validation framework. For jQuery, jQuery Validate usually works really well. It supports things like dependencies between different fields, etc. And you can also quite easily add new rules, like valid ISBN numbers, etc.
Edit: As for your code, I'm not sure that you can use onunload for this, as at that point there's no way back, you can't abort at that point. You should put this code on the onsubmit event instead.

Categories

Resources