Adding / Removing Dynamically Generated Events in JavaScript - javascript

The problem I've got is trying to dynamically generate answers into pre-made boxes for a JavaScript quiz. Each answer has an index and it's the index that determines if the answer is correct (this info comes from a JSON and can't be modified). For each question, the answers are all randomised. I also have a seperate function to feed back t the user if they were right or wrong.
The approach I initally took for to use a For Loop to dynamically build each answer, but this was loading the same index into each answer (a common problem when using variables inside for loops). So I decided to add a listener to each answer and pulled this out into a seperate function to ensure each answer had it's own index. That part worked great, each had it own listener passing the index to the answer function, but when the next question was loaded two listeners had been added to the answer as it was re-generated for the next question. So, logically I added a remove listener, but this doesn't seem to work.
I know what I need to do to make this quiz work, I'm just not sure how to do it. Here's the code I have already:
Loading Answers to screen:
// Load Answers
for (i=0; i<4; i++) {
var answerBox = 'answer' + (i + 1);
app.LoadInnerHTML(answerBox, answers[i].title);
this.listenerFunc(i);
}
Adding the Listeners:
this.listenerFunc = function(j) {
var thisQuiz = this;
var answers = this.CurrentAnswers;
var answerBox = 'answer' + (j + 1);
var answerIndex = answers[j].index;
var answerElement = document.getElementById(answerBox);
// If there already is an event listener then remove it
answerElement.removeEventListener('click', function () {
thisQuiz.AnswerQuestion(answerIndex);
});
// Add New Event Listener
if (answerElement.addEventListener) { // all browsers except IE before version 9
answerElement.addEventListener("click", function () {
thisQuiz.AnswerQuestion(answerIndex);
}, false);
} else {
if (answerElement.attachEvent) { // IE before version 9
answerElement.attachEvent("click", function () {
thisQuiz.AnswerQuestion(answerIndex);
});
}
}
};
Answering the Question:
// Answer Questions
this.AnswerQuestion = function (index) {
// If Question is answered correctly
if (index == 0) {
alert('Question Answered Correctly ' + index);
} else {
alert('Question Answered Incorrectly ' + index);
}
// Call Next Question
this.LoadNextQuestion();
}
I just feel like I need to untangle everything becasue I've kept patching it to try and make it work. As a side note though I can only work with JavaScript, I can't use any frameworks like JQuery - I'm pretty sure theres a really easy solution in JQuery, but unfortunately I'm bound to just JavaScript.

The problem is with passing closure to removeEventListener. It's a completely new reference to callback so browser is not able remove it, because it's not defined in listeners list.
You need to extract somewhere outside (away from listenerFunc) a list of listeners:
this.listenerCallbacks = [];
this.listenerFunc = function(j) {
// .. head of listenerFunc
var answerIndex = answers[j].index;
if (!this.listenerCallbacks[answerIndex]) {
this.listenerCallbacks[answerIndex] = function () {
thisQuiz.AnswerQuestion(answerIndex);
}
}
var answerElement = document.getElementById(answerBox);
// If there already is an event listener then remove it
answerElement.removeEventListener('click', this.listenerCallbacks[answerIndex]);
// Add New Event Listener
if (answerElement.addEventListener) { // all browsers except IE before version 9
answerElement.addEventListener("click", this.listenerCallbacks[answerIndex], false);
} else if (answerElement.attachEvent) { // IE before version 9
answerElement.attachEvent("click", this.listenerCallbacks[answerIndex]);
}
};

Related

How does 'addEventListener` work behind the scenes? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
So, I have had this curiosity for quite a while now. I want to know, how addEventListener works behind the scenes. I know what it does but I am just not able to get my head around how it does it.
I have checked many links and resources and this is the one that was closest to what I was looking for but still no success.
To clarify on what I am really looking for, I want to know how can one create his own addEventListener function that would take the first argument as the event name and the second argument as the callback that would accept the eventArgs argument.
This would be simple example of event dispatching system
class BusEvent {
eventName = null;
callbacks = [];
constructor(eventName) {
this.eventName = eventName;
}
register(callback) {
this.callbacks.push(callback);
}
unregister(callback) {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}
execute(data) {
const callbacks = this.callbacks.slice(0);
callbacks.forEach((callback) => {
callback(data);
});
}
}
class MessageBus {
constructor() {
this.events = {};
}
dispatch(eventName, data) {
const event = this.events[eventName];
if (event) {
event.execute(data);
}
}
on(eventName, callback) {
let event = this.events[eventName];
if (!event) {
event = new BusEvent(eventName);
this.events[eventName] = event;
}
event.register(callback);
}
off(eventName, callback) {
const event = this.events[eventName];
if (event && event.callbacks.indexOf(callback) > -1) {
event.unregister(callback);
if (event.callbacks.length === 0) {
delete this.events[eventName];
}
}
}
}
Usage:
const messageBus = new MessageBus();
messageBus.on('ReceiveData', (data) => {
console.log(data);
})
messageBus.dispatch('ReceiveData', { name: 'Ted' });
// console logs { name: 'Ted' }
As far as I know JS has kind of a Observer, which is a design pattern from GoF (Gang of Four), you can read more here. The Observer will keep waiting for the state change, and then when it's changed the callback will be called notifying all the objects. The UML bellow will give you an idea of how it works.
So, I do think JS works on that way, but instead of all objects be the same each one will have a custom callback, or more than one callback.I'm not pretty sure about that, but should works that way. I didn't found anything online, but this Reddit topic talks about it
If you want to add a custom event to the DOM, I don't think that is possible. At least i'm not aware of any specification to support that. There is the custom element spec that allows you to create your own web components. In doing that you can check which events could be fired. However, you can do the same concept outside of the DOM:
var callbacks = [];
function addToEventListener(callback) {
callbacks.push(callback);
}
function triggerMyEvent(payload) {
callbacks.forEach(function(cb){
cb(payload);
});
}
addToEventListener(function(p){
console.log(p);
});
triggerMyEvent({testValue:true});
Note: the above uses globals, which isn't a good practice. I would recommend against that, but for brevity of this post, I chose to use globals. The ideas are the same.
addEventLisner bind the handle with html DOM node. It takes the event that to be attach and one callback function.

Confirming that using a factory is the best (only?) way to create a generic multi-use click count listener

I have created the following click count factory for adding the click count to the event information already present in a regular click event. This function creates a clickCountObj to track the number of clicks, as well as a new function for catching a click event on the given element parameter and reporting it back to the listener parameter along with the click count.
Originally, I wanted to do this as a class, rather than a factory... Way back when I was working in Java, I would have done it with a façade class, so that's what I was thinking. But I've concluded that it is not possible in Javascript, because the same function you'd use to create the object would be the one called in response to the click, and I can't see a way around this.
The purpose of this question is simply to improve my understanding and using of JavaScript. Please let me know if I am wrong in my conclusion stated above, or if there are any other alternatives to doing this a better way?
function clickCount(element, listener) {
let clickCountObj = {};
clickCountObj.clickCount = 0;
clickCountObj.clickDelay = 500;
clickCountObj.element = element;
clickCountObj.lastClickTime = 0;
let clickCountListener = function (e) {
// alert("last click time: " + clickCountObj.clickDelay);
if ((e.timeStamp - clickCountObj.clickDelay) < clickCountObj.lastClickTime) {
clickCountObj.clickCount = clickCountObj.clickCount + 1;
// alert("click count up: " + clickCountObj.clickCount);
}
else {
clickCountObj.clickCount = 1;
}
clickCountObj.lastClickTime = e.timeStamp;
listener.call(element, clickCountObj.clickCount, e);
};
if (!element) throw "No element to listener to";
element.addEventListener("click", clickCountListener);
return clickCountListener;
}
For sure you can also use a class:
class ClickCounter {
constructor(element, onClick, delay = 500) {
this.element = element;
this.onClick = onClick;
this.counter = 0;
this.delay = delay;
this.lastClicked = 0;
element.addEventListener("click", () => this.click(), false);
}
click() {
if(Date.now() < this.lastClicked + this.delay)
return;
this.lastClicked = Date.now();
this.onClick.call(this.element, this.counter++);
}
}
new ClickCounter(document.body, count => {
alert(count);
});
[is] doing this a better way?
No, not really. Using a class is not really useful here as you don't want to expose properties and you also don't need inheritance. A factory seems to be a good approach here.
Small sidenote: Instead of
return clickCountListener;
it would make more sense to
return clickCountObj;
as it would expose the settings and the count which might be useful.
warning: unserious content below
Way back when I was working in Java ...
... you took over that senseless naming scheme (clickCountObj.clickCount). I guess you won't loose any necessary information with just settings.count ...

Function call inside a for loop breaks the loop

I have a small fiddle I was experimenting with, and I noticed calling a function inside a for loop condition was stopping the loop. Basically, initially I wanted to do this:
// add event listeners to tabs
for (i=0;i<tabs.length;i++) {
tabs[i].addEventListener('click', function(event) {
var tab = event.target;
selectPage(tab.dataset.tab);
changeTab(tab);
});
if (tabs[i].classList.contains('active')) {
selectPage(tabs[i].dataset.tab);
}
}
But, ended up having to do this to make it work:
// add event listeners to tabs
for (i=0;i<tabs.length;i++) {
tabs[i].addEventListener('click', function(event) {
var tab = event.target;
selectPage(tab.dataset.tab);
changeTab(tab);
});
}
// find active class and set page
for (i=0;i<tabs.length;i++) {
if (tabs[i].classList.contains('active')) {
selectPage(tabs[i].dataset.tab);
}
}
Here is a link to the Fiddle
Thanks for any help in advance, I feel there is something fundamental here I'm not getting. Thanks
Lesson 0: use ESLint or similar tools to check your code for trivial errors before spending sleepless nights here on SO and/or in debugging tools.
Lesson 1: localize your variables.
Your problem is with variable i that's global - hence reused by both your global code and selectPage function. The latter sets its value to tabs.length, ending up the loop prematurely.
Just replace i = 0 with var i = 0 at each for expression.
Try declaring the x variable using let.
// add event listeners to tabs
for (let i=0;i<tabs.length;i++) {
tabs[i].addEventListener('click', function(event) {
var tab = event.target;
selectPage(tab.dataset.tab);
changeTab(tab);
});
if (tabs[i].classList.contains('active')) {
selectPage(tabs[i].dataset.tab);
}
}

JavaScript variables do not persist in jQuery event handler [duplicate]

This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 8 years ago.
For some reason, I'm not understanding the scope of local and global variables with relation to jQuery click handlers. When I click the buttons, I'm getting "undefined", but I expect it to show the category of the button clicked. How do I address this?
I've setup an example to show my confusion. Fiddle here: http://jsfiddle.net/zvhsqeyn/
Simple html buttons:
<button class="hobbies">hobbies</button>
<button class="sledding">sledding</button>
<button class="personal">personal</button>
<button class="food">food</button>
Some javascript to handle the event where the buttons are clicked.
var GLOBAL_CATEGORIES = ['hobbies', 'sledding'];
$(window).load(function() {
var categories = ['personal', 'food'];
// local categories
for (var i = 0; i < categories.length; i++) {
$('.' + categories[i]).click(function() {
alert(categories[i]);
return false;
});
}
// Global categories
for (var i = 0; i < GLOBAL_CATEGORIES.length; i++) {
$('.' + GLOBAL_CATEGORIES[i]).click(function() {
alert(GLOBAL_CATEGORIES[i]);
return false;
});
}
});
Edit:
After taking in the feedback, here is the properly working http://jsfiddle.net/zvhsqeyn/1/
This has to do with the nature of closures.
// local categories
for (var i = 0; i < categories.length; i++) {
$('.' + categories[i]).click(function() {
alert(categories[i]);
return false;
});
}
You're assigning an anonymous function as the click handler for each of these buttons. That function has access to the variable i, because it's visible in the parent scope. However - by the time you click one of these buttons, your loop has long since finished, and the value of i is now equal to categories.length. There isn't any element there, so your click functions return undefined.
In general, it's not safe to rely on the value of index variables when assigning event handlers in a loop. As a workaround, you can use the jquery $.each function:
$.each(categories, function(index, value) {
// local categories
$('.' + value).click(function() {
alert(value);
return false;
});
});
This works because it creates a separate closure for each click handler function.
This is a very common ish problem and here is the explanation
If we follow the process through what you are doing is adding a click listener to the ith GLOBAL_CATEGORIES and then moving on to the next one. You do this until i is too big for the GLOBAL_CATEGORIES array.
You have to remember that i has only been set once and so now i is 1 more than the length of GLOBAL_CATEGORIES. This means that when the alert fires GLOBAL_CATEGORIES[i] is undefined because i is too big.
A better way to do this is with the jQuery each syntax like below
$.each(GLOBAL_CATEGORIES, function(i, category) {
$('.' + category).click(function() {
alert(category);
return false;
});
});

Spinlock in Javascript

How can I do a spinlock in javascript?
I'm trying to load a bunch of images and I can only move forward after everything is loaded, so I have a spinlock like
for(...)
image[i].onload = function() { ++imagesloaded; }
while(imagesloaded != totalimages)
{
}
And it crashes my browser. Is there a better way to do it? Or a yield / sleep function I'm missing?
Short answer: don't spinlock.
Longer answer: here's how to do it:
var imagesLoaded = 0;
var totalImages = 42;
function handleImageLoad()
{
imagesLoaded++;
if (imagesLoaded === totalImages)
{
doSomething();
}
}
for (var i=0; i<totalImages; i++)
{
image[i].onload = handleImageLoad;
}
In general, when you want to sleep/wait/spin in JavaScript, instead think about solving the problem in terms of callbacks (and setTimeout/setInterval).
The answers above aren't useful as spinlocks may be required because of limitations/bugs in browsers. For instance safari (hopefully not future versions) requires the use of method window.open when you want to generate a file in javascript. The consequence of this is that you cannot generate the file using any callbacks (because of popup blockers), this in effect forces the use of a dialog window that first calls the file generation function (using callbacks) and then a button that downloads the file. Because spinlocks don't work the code becomes the following:
function process(callback) {
processCallbackData = null; // global var that must be a unique name
callback(function(data) {
processCallbackData = data;
});
}
function fowardButton() {
if(processCallbackData!=null) {
goForwardUsingCallbackIncompatibleCode();
} else {
displayStillLoadingWarning();
}
}
Don't use a loop to check. Check in the event handler function. (So you only do the check when an image has loaded, not continuously and as quickly as possible)

Categories

Resources