Event firing multiple times - javascript

I have a little problem that I replicated in the little code snippet below (in the most simple way possible, however it still shows the problem I am facing).
Here is the snippet :
const searchBar = document.getElementById('search');
const resBox = document.getElementById('results');
searchBar.addEventListener('input', function handler(e) {
if (e.target.value === '') {
resBox.innerHTML = '';
return false;
}
setTimeout(() => populate(e), 300);
});
function populate(e) {
const btnBox = document.createElement('div');
for (let i = 1; i <= 3; i++) {
const btn = document.createElement('button');
btn.classList.add('js-click')
btn.innerText = 'Click me';
btnBox.appendChild(btn);
}
resBox.appendChild(btnBox);
dynamicBtnClickListener();
}
function dynamicBtnClickListener() {
resBox.addEventListener('click', function handler(e) {
console.log('You clicked me !');
});
// THE SOLUTION I FOUND FOR THE MOMENT :
//const btns = document.querySelectorAll('button.js-click');
//btns.forEach(btn => {
// btn.addEventListener('click', function handler(e) {
// console.log('You clicked me !');
// });
//});
}
<input type="text" id="search">
<div id="results"></div>
As you can see in the snippet, I have a first listener on input that generates a list of buttons when you type in it. When it is empty, the buttons disappear. In my real world case, it is a search input, that when a user types in, calls a function that populates a result box below it with results from DB.
I then have an on click listener on the buttons. In the code snippet, I simply put a console('You clicked me') when you click on the buttons. In my real app, it takes the result item (each result is an user) and inserts it in a table.
The problem appears when you open, close, then re-open the results box. This is done by inputing something, clearing the input, then re-input something. When you do that and then click on one of the buttons, it fires the click event on them as many times as you opened / closed the result box, so you will see the "You clicked me" on console multiple times.
I have done some research and most of it calls for using event.stopPropagation() and / or removing the event listener(s). I did try all these possible solutions, in every way I could think of, but I couldn't make it work.
Anyways I found a way around this (the commented portion of the dynamicBtnClickListener() function), but I feel it is not optimal. It consists of getting all the buttons with querySelectorAll(), then loop through them and add the click listener to every one of them, but I do not think it is optimal nor best-practice like. This is why I come here to ask if maybe there is a better solution, possibly one that keeps the click listener on the results box (if that is the most optimal solution. Is it by the way ?).
So even though I found a solution to this problem, could someone please tell me what is the best practice and optimal way of doing this ?
Thank you very much for your help

Each time the you type in the text area, resBox is accessed each time and the actual element resBox gets a new event listener every time(the buttons don't have any specific listener themselves, so I make EACH BUTTON have a specific listener individually)
const searchBar = document.getElementById('search');
const resBox = document.getElementById('results');
searchBar.addEventListener('input', function handler(e) {
if (e.target.value === '') {
resBox.innerHTML = '';
return false;
}
setTimeout(() => populate(e), 300);
});
function populate(e) {
const btnBox = document.createElement('div');
for (let i = 1; i <= 3; i++) {
const btn = document.createElement('button');
btn.classList.add('js-click')
btn.innerText = 'Click me';
btn.addEventListener('click',function(ev){console.log('You clicked me !')})
btnBox.appendChild(btn);
}
resBox.appendChild(btnBox);
}
<input type="text" id="search">
<div id="results"></div>
Now, here is an example that only has one event listener but would completely handle the situation >:D
Technically this should be faster(since one event listener compared to many), but personally I prefer this option because it "feels better" due to one function controlling the whole button layout(which would make it less "rigid")
PS: The speed difference is so insignificant, you can pick and choose(but if a whole chuck ton of buttons, yea this becomes better)
const searchBar = document.getElementById('search');
const resBox = document.getElementById('results');
const randChars=()=>{ //random characters to prove ONE event listener can work for multiple buttons in resBox
let arr=["a","b","c","d","e"]
let randIndex=()=>Math.floor(Math.random()*arr.length)||1
let n=randIndex(); let returnChar=""
for(let i=0;i<n;i++){returnChar+=arr[randIndex()]}
return returnChar
}
searchBar.addEventListener('input', function handler(e) {
if (e.target.value === '') {
resBox.innerHTML = '';
return false;
}
setTimeout(() => populate(e), 300);
});
resBox.addEventListener('click',function(ev){ //single event listener for all buttons
let currentButton=ev.path[0]
if(currentButton.tagName!="BUTTON"){return;} //if a button was not clicked
console.log("Button with text\n'"+currentButton.innerText+"'\nwas clicked")
})
function populate(e) {
const btnBox = document.createElement('div');
for (let i = 1; i <= 3; i++) {
const btn = document.createElement('button');
btn.classList.add('js-click')
btn.innerText = 'Click me '+randChars();
btnBox.appendChild(btn);
}
resBox.appendChild(btnBox);
}
<input type="text" id="search">
<div id="results"></div>

Related

Javascript - Creating event listener that returns id of dynamically generated content

my main project is too complicated to show here so I created a small script demonstrating the problem I am working on. In simple terms, I need to create a button that once clicked, generates a button that also has an event listener that returns that button's id to the console.
See code below:
button_number = 0
create_buttons = document.getElementById('create_buttons')
div = document.getElementById('div')
create_buttons.addEventListener('click', e=>{
button_number += 1
new_button = document.createElement('button')
new_button.setAttribute('id', 'button'+button_number)
new_button.innerHTML = 'What number am I?'
new_button.addEventListener('click', show_button_number)
div.appendChild(new_button)
})
function show_button_number () {
let number = button_number
button = document.getElementById('button' + number)
console.log(button.id)
}
<div id="div">
<button id="create_buttons">Create a button!</button>
</div>
As written, all generated buttons return the button id of the most recently generated button versus their own id. Is there anyway I can change the anonymous function to return the button id of the button that was clicked? In order to integrate this into my main project, I need to create the event listener for the dynamically generated buttons using an anonymous function.
You could make a higher-order function, one that takes the current button number as an argument and returns a function using it:
const makeListener = num => () => {
const button = document.getElementById('button' + num)
console.log(button.id)
};
new_button.addEventListener('click', makeListener(button_number))
Or, you may not need the ID at all, just pass the element itself:
create_buttons.addEventListener('click', e=>{
const btn = div.appendChild(document.createElement('button'));
btn.textContent = 'What number am I?'
btn.addEventListener('click', () => {
console.log(new_button);
});
});
The major way to do that is to use event delegation mechanim
const divParent = document.getElementById('div')
var button_number = 0
divParent.addEventListener('click', e =>
{
if (!e.target.matches('button')) return // ignore clicks from other things
if (e.target.id === 'create_buttons')
{
let new_button = document.createElement('button')
new_button.id = 'button' + ++button_number
new_button.textContent = 'What number am I?'
divParent.appendChild(new_button)
}
else
{
console.clear()
console.log( e.target.id )
}
})
<div id="div">
<button id="create_buttons">Create a button!</button>
</div>

Javascript - Issue on event listeners of classList

I have a "+" button that , when clicked, triggers the creation of a block with an input and 2 buttons, one for validating the input and one for removing it.
// My code looks almost like this :
addBtn.addEventListener('click', e => {
addClick++;
// All the elements of the same line (input and 2 buttons) have an int in common in their id string ==> addClick
// I'm missing all the declarations of the variables here
blockDiv.appendChild(posteInput);
blockDiv.appendChild(validateBtn);
blockDiv.appendChild(deleteBtn);
globalPostesBlock.appendChild(blockDiv)
let allDeleteBtn = document.getElementsByClassName('delete-button');
for (let i = 0; i < allDeleteBtn.length; i++) {
allDeleteBtn[i].addEventListener('click', e => {
// Retrieving the block with the same id
let deleteBtnId = parseInt((allDeleteBtn[i].getAttribute('id').match(/\d+/g)).toString());
let singlePosteBlock = document.getElementById(`poste-block-${deleteBtnId}`);
singlePosteBlock.remove();
}
})
}
The event listener represents the action of clicking the delete button so it can remove its entire containing block
I have the same logic for the validate button, but I'm using ajax in it.
Each time I create a new block, I want to create an event listener associated to this block, but all I found so far is an event listener with a loop on every buttons, so what happens is that the action triggers as many time as the block numbers because of the loop, but I don't know how to dissociate every event listeners.
If I have 3 blocks and I validate one input value which is being inserted in the DB afterwards, the value is being inserted 3 times.
Does this help ?
//id pool
let latestId = 0;
//retrive the button
var myButton = document.querySelector("button");
myButton.addEventListener("click", createKids);
//function declaration :: createKids
function createKids() {
latestId++;
//declare and initialization
let div = document.createElement("div");
let input = document.createElement("input");
let buttonSend = document.createElement("button");
let buttonDelete = document.createElement("button");
//append input & buttons to div
div.appendChild(input);
div.appendChild(buttonSend);
div.appendChild(buttonDelete);
//Some beautifying
buttonSend.innerText = "Send me";
buttonDelete.innerText = "Delete me";
//some logic
div.dataset.id = latestId;
//event handeling
buttonSend.addEventListener("click", sendItem);
buttonDelete.addEventListener("click", deleteItem);
//insert div
document.body.appendChild(div);
}
function sendItem(event) {
//do action and delete ?
let input = event.target.parentNode.querySelector("input");
//retrive data
let val = input.value;
let id = event.target.parentNode.dataset.id;
//disable input for fun ^^
input.disabled = true;
//console istead of send
console.log(id,val);
//handle some more
setTimeout(() => {
event.target.parentNode.remove();
}, 3000);
}
function deleteItem(event) {
event.currentTarget.parentNode.remove();
}
<p>Does this help?</p>
<button>Magic Button</button>

Facing problem in JavaScript function execution

I am trying to make a simple Shopping List App in which user can Add, Delete and mark the task done when completed. So far, I am able to add the task but facing problem in executing the done and delete functions. I am getting an error because when I execute it, the done and delete buttons are not there but what should I do to fix it?
var inp = document.getElementById("form");
var button = document.getElementById("click");
//Create List Function with Done and Delete Buttons
function addVal() {
var ul = document.getElementById("list");
var li = document.createElement("li");
var span = document.createElement("span");
var done = document.createElement("button");
var del = document.createElement("button");
li.appendChild(document.createTextNode(""));
span.appendChild(document.createTextNode(inp.value));
done.appendChild(document.createTextNode("Done"));
del.appendChild(document.createTextNode("Delete"));
li.appendChild(span);
li.appendChild(done);
li.appendChild(del);
done.setAttribute("class", "doneBut");
del.setAttribute("class", "delBut");
ul.appendChild(li);
inp.value = "";
}
//Get Input Length
function checkLength() {
return inp.value.length;
}
//Run function on Button Click
function onButtonClick() {
if (checkLength() > 0) {
addVal();
}
}
//Run function on Enter Keypress
function onEnter(event) {
if (checkLength() > 0 && event.which === 13) {
addVal();
}
}
//Trigger Events
button.addEventListener("click", onButtonClick);
inp.addEventListener("keypress", onEnter);
//Done and Delete Button Functions
var doneButton = document.getElementsByClassName("doneBut");
var deleteButton = document.getElementsByClassName("delBut");
function doneTask() {
doneButton.parentNode.classList.add("done");
}
function delTask() {
deleteButton.parentNode.classList.add("delete");
}
doneButton.addEventListener("click", doneTask);
deleteButton.addEventListener("click", delTask);
<input type="text" placeholder="Enter Your Task..." id="form" />
<button id="click">Add Task</button>
<h2>List:</h2>
<ul id="list"></ul>
Please Help.
Your problem is that the code tries to add events before the buttons exist. The buttons don’t exist until the addVal function gets called. Since addVal is not being called before the you try to add your event handlers, the getElementById returns null, and you attempt to add an event listener to null.
Additionally it looks like you’re planning to add multiple done and delete buttons. That wouldn’t normally be a problem, except you’re referencing them by ID, and IDs MUST be unique. You’ll need to switch this to a class or an attribute, since you’ll need one per item in the shopping cart.
You’ll probably want to look into event delegation, so that you can add your events once to the page before any buttons exist. https://javascript.info/event-delegation
It's most likely because your script is running before your code is running. Add the <script> tags just before the closing </body> tag to fix it:
<script>/* Your code here */</script>
</body>
You need to place this in a window.onload function, or run it in a function inside of the body tag's onload. Those elements don't exist yet when the script is run:
window.onload = function() {
var inp = document.getElementById("form");
var button = document.getElementById("click");
button.addEventListener("click", onButtonClick);
inp.addEventListener("keypress", onEnter);
}

What is this piece of Javascript for Isotope filter doing?

I am having some trouble understanding what is happening in a piece of vanilla JS for the Isotope filter. The original code is here: https://codepen.io/desandro/pen/VWLJEb
var buttonGroups = document.querySelectorAll('.button-group');
for (var i = 0; i < buttonGroups.length; i++) {
var buttonGroup = buttonGroups[i];
var onButtonGroupClick = getOnButtonGroupClick(buttonGroup);
buttonGroup.addEventListener('click', onButtonGroupClick);
}
function getOnButtonGroupClick(buttonGroup) {
return function(event) {
// check for only button clicks
var isButton = event.target.classList.contains('button');
if (!isButton) {
return;
}
var checkedButton = buttonGroup.querySelector('.is-checked');
checkedButton.classList.remove('is-checked')
event.target.classList.add('is-checked');
}
}
What is happening between the getOnButtonGroupClick function and it being assigned to a variable in the for loop preceding it?
getButtonGroupClick returns a closure that saves the value of buttonGroup. When you click on a button in the button group, it uses that closure variable to search for the checked button in the group, uncheck it, and then check the button you clicked on.
This complexity isn't really needed. When an event listener is called, event.currentTarget is set to the element that the listener was attached to, so you could just use that.
var buttonGroups = document.querySelectorAll('.button-group');
for (var i = 0; i < buttonGroups.length; i++) {
var buttonGroup = buttonGroups[i];
buttonGroup.addEventListener('click', onButtonGroupClick);
}
function OnButtonGroupClick(event) {
// check for only button clicks
var isButton = event.target.classList.contains('button');
if (!isButton) {
return;
}
var checkedButton = event.currentTarget.querySelector('.is-checked');
checkedButton.classList.remove('is-checked')
event.target.classList.add('is-checked');
}
The for loop is used to iterate over all of the elements with the class of button-group and and a click event listener to them. getOnButtonGroupClick returns a function to be used as the function to be used as the event listener for the element i.e. the function that is run when the element is clicked on.
var buttonGroups = document.querySelectorAll('.button-group');
//get all elements within the document with a class of button-group
//buttonGroups is a NodeList
for (var i = 0; i < buttonGroups.length; i++) {
//loop through all of the elements with a class of button-group matched by the above query selector
var buttonGroup = buttonGroups[i];
//get the element in the NodeList with the index i
var onButtonGroupClick = getOnButtonGroupClick(buttonGroup);
//get the function to be run when the element is clicked on
buttonGroup.addEventListener('click', onButtonGroupClick);
//add a click event listener to the element
}
function getOnButtonGroupClick(buttonGroup) {
return function(event) {
// check for only button clicks
var isButton = event.target.classList.contains('button');
//check if the element has a class of button
if (!isButton) {
//if the element does not have a class of button, do nothing
return;
}
var checkedButton = buttonGroup.querySelector('.is-checked');
checkedButton.classList.remove('is-checked')
event.target.classList.add('is-checked');
}
}
If I understood your question correctly, It means that a click event is being added to every button in the buttonGroups there is. Although, if you ask me, it would be way better and cleaner to just use a forEach, like so:
const buttonGroups = document.querySelectorAll('.button-group');
buttonGroups.forEach(button => button.addEventListener("click", OnButtonGroupClick)
function OnButtonGroupClick(event) {
// check for only button clicks
let isButton = event.target.classList.contains('button');
if (!isButton) {
return;
}
let checkedButton = event.currentTarget.querySelector('.is-checked');
checkedButton.classList.remove('is-checked')
event.target.classList.add('is-checked');
}
So, you add a click event to ALL the buttons in the buttonGroups that will run the function onButtonGroupClick.
EDIT: And there's no really need to assign the function like that... at all. Just call it on the click event and that's it.

Re-writting a string with a click event

I am trying to program a click function that if a certain condition is true, each time the button is clicked it will overwrite the current string and replace it with a new string character.
I created a simple example below to illustrate what I am trying to accomplish.
$(document).ready(function() {
const hello = document.getElementById("hi");
const button =
document.getElementById("replace");
let clicked = false;
let goodBye = function() {
clicked = true;
if (hello.innerHTML.length < 9) {
if (clicked) {
// I want to clear the current HTML first, then I went the new HTML to add a single 9 every time the button is clicked.
hello.innerHTML += "9";
}
}
}
button.addEventListener("click", goodBye);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1 id="hi">01234567</h1>
<button id="replace">click me</button>
While I'm sure this is very simple I'm still relatively new to working with JS and I've been stuck trying to figure this out for over a week now. Any help is appreciated.
I removed your jQuery as it's not needed. I believe this is what you are looking for.
Steps:
Obtain a reference to your elements (hello and button)
Bind an event listener to your button click
When button is clicked, get the current value of your hello element
Run logic (if statement) and do what you need.
https://jsfiddle.net/3ho2by8t/14/
(() => {
const hello = document.getElementById("hi");
const button = document.getElementById("replace");
button.addEventListener('click', (evt) => {
const helloText = hello.innerHTML;
if (helloText.length > 9) {
hello.innerHTML = '9';
} else {
hello.innerHTML += helloText.length;
}
});
})();
<h1 id="hi">01234567</h1>
<button id="replace">click me</button>

Categories