Event delegation not working on dynamic buttons - javascript

No matter what I try, the .onclick or addEventListener 'click' will not work on my dynamically created buttons and I can't figure out why. As I was looking for solutions, I came across Event Delegation and I looked through 3 different websites and looked at the examples. I was sure this was going to solve my problem and I tried to mimic the examples but still it isn't working. I posted a question on here earlier but it was immediately removed because apparently it was too similar to another question (that was 12 years old!) but when I looked at that question they were using jQuery. I'm still a beginner in JS so I would prefer to understand how to resolve this in plain JS and I'm hoping this won't be removed.
This is my code:
document.addEventListener('DOMContentLoaded', function() {
userData();
document.querySelector('.list-group').addEventListener('click', function(e) {
if(e.target && e.target.nodeName == "BUTTON"){
console.log(e.target.id);
}
});
})
function userData() {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(users => {
const h6 = document.createElement("h6");
h6.innerText = "List of Users";
const userList = document.createElement("div");
userList.className = "list-group";
users.forEach(function(user) {
const userButton = document.createElement("button");
userButton.className = "list-group-item list-group-item-action";
userButton.id = `${user.id}`;
userButton.innerHTML = `
<strong>${user.name}</strong><br>
${user.email}<br>
${user.address.city}<br>
`;
userList.appendChild(userButton);
});
const container = document.querySelector('#response');
container.appendChild(h6);
container.insertBefore(userList, h6.nextSibling);
});
}
function userSelect(user_id) {
fetch(`https://jsonplaceholder.typicode.com/users/${user_id}`)
.then(response => response.json())
.then(user => {
console.log(user);
});
}
What I have now is a list of users and ultimately I want to be able to click on a user and bring up the full details of that user. At first I was trying to use the onclick function to redirect to the userSelect function but when that failed I looked around and found Event Delegation and still no luck. I tried to move the document.querySelector('.list-group) section down at the end of the userData function and still no luck. When I click on a button nothing shows up in console, if I use the userSelect function directly in console a user object appears. I'm at a real loss on how to get this to work. Please help!

Since function userData is making asynchronous call, the issue seems to be that you are adding the click event handler before the element with class '.list-group' got created.
You should use something like this to add click handler
document.addEventListener('DOMContentLoaded', function () {
userData().then(response => {
document.querySelector('.list-group').addEventListener('click', function (e) {
if (e.target && e.target.nodeName == "BUTTON") {
console.log(e.target.id);
}
})
});
})
Try below snippet:
document.addEventListener('DOMContentLoaded', function() {
userData().then(response => {
document.querySelector('.list-group').addEventListener('click', function(e) {
if (e.target && e.target.nodeName == "BUTTON") {
console.log(e.target.id);
}
})
});
})
function userData() {
return fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(users => {
const h6 = document.createElement("h6");
h6.innerText = "List of Users";
const userList = document.createElement("div");
userList.className = "list-group";
users.forEach(function(user) {
const userButton = document.createElement("button");
userButton.className = "list-group-item list-group-item-action";
userButton.id = `${user.id}`;
userButton.innerHTML = `
<strong>${user.name}</strong><br>
${user.email}<br>
${user.address.city}<br>
`;
userList.appendChild(userButton);
});
const container = document.querySelector('#response');
container.appendChild(h6);
container.insertBefore(userList, h6.nextSibling);
});
}
<div id="response">
</div>
or you can move the addEventListener code to end of userData

Related

adding click event to promise

I am having trouble with adding a click event to an element that only exists if another element on the page has been clicked. This is on the Amazon website just for context. I am trying to add an overlay at the bottom of the mobile screen which allows you to select the quantity you would want and add it to basket.
In order to do this I have created my own dropdown which allows you to select the quantity (selectNumber function) - what this does is replicate the click on the original dropdown and bring up the popover element which you click in order to select the quantity. I then targeted the element in the popover which represents the quantity you would like and added a click event on that too.
The issue I am having is that on the first time I click on my dropdown and change the quantity it replicates the click on the original dropdown but doesn't update the quantity. However, if I try it again after it works perfectly. I have a feeling that it is to do with the fact the popover does not exist until the first click of the dropdown, it then fails to fire the rest of my code. I am using a Promise function (waitforElem) in order to observe that the element now exists on the page. The second time - as the element now exists - it is able to fire the rest of my code, which allows you to update the quantity, just fine. I have tried to put all my code in the same .then function but in the below I have split it into two .then's. Neither seems to work. My code is below:
function waitForElm(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector))
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector))
observer.disconnect()
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
})
}
selectNumber().forEach((el) =>
el.addEventListener('input', () => {
const numberSelected = Number(el.options[el.selectedIndex].value)
console.log(numberSelected)
amazonDropDown().click()
const popoverDropdown = () =>
document.querySelectorAll(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper .a-nostyle.a-list-link .a-dropdown-item'
)
waitForElm(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper '
)
.then(() => {
console.log('Element is ready')
})
.then(() => {
document.querySelectorAll(
'.a-popover.a-dropdown.a-dropdown-common.a-declarative .a-popover-wrapper .a-nostyle.a-list-link .a-dropdown-item'
)
const popoverArr = () => Array.from(popoverDropdown())
const popoverEl = () =>
popoverArr().filter((el) => {
const numb = () => Number(el.firstElementChild.textContent)
return numb() === numberSelected
})
console.log(popoverEl())
for (let i = 0, len = popoverEl().length; i < len; i++) {
const firstChild = (): any => popoverEl()[i].firstElementChild
console.log(firstChild())
firstChild().click()
}
})
})
)
I really hope the above makes sense and that someone can help me on this as I have been banging my head on it for a whole day. Thank you in advance.

Problem pulling json content with variable url

I hope someone can help me out with this one
My question is why does this code do exactly what I need?
var wfComponent;
fetch("https://nube-components.netlify.app/navbar01.json")
.then((res) => res.text())
.then((data) => (wfComponent = data))
.then(() => console.log(wfComponent));
document.addEventListener("copy", function (e) {
e.clipboardData.setData("application/json", wfComponent);
e.preventDefault();
});
document.getElementById("navbar01").onclick = function () {
document.execCommand("copy");
};
And this one does not do the copy to clipboard part?
$(".button.copy-button").on("click", function () {
let tag = $(this).attr("id");
console.log(tag);
var wfComponent;
fetch("https://nube-components.netlify.app/" + tag + ".json")
.then((res) => res.text())
.then((data) => (wfComponent = data))
.then(() => console.log(wfComponent));
document.addEventListener("copy", function (e) {
e.clipboardData.setData("application/json", wfComponent);
e.preventDefault();
});
document.getElementById(tag).onclick = function () {
document.execCommand("copy");
};
});
Now as you can see what I need is to "automate" that JSON location and target button part where I need each button to target a different URL. So I am now lost in this area where I manage to pull that id and apply it to the URL but the content does not get copied to the clipboard.
I am not a JS expert at all so please feel free to pinpoint anything I might be doing wrong or any ways to do this completely differently
Thanks
Because you use addEventListener inside the other. But either way, there is another (possibly hacky) way to achieve this.
$(".copy-button").on("click", function(e) {
let tag = $(this).attr("id");
fetch("https://nube-components.netlify.app/" + tag + ".json")
.then((res) => res.text())
.then((data) => (wfComponent = data))
.then(() => {
let copyFrom = document.createElement("textarea");
document.body.appendChild(copyFrom);
copyFrom.textContent = wfComponent;
copyFrom.select();
document.execCommand("copy");
copyFrom.remove();
console.log('COPIED!');
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="navbar01" class="copy-button">copy navbar01</button>
<button id="navbar02" class="copy-button">copy navbar02</button>

Javascript How to remove event that was added, and be able to add it again when need to?

I don't know to explain this, but let me try. I'm currently doing a snooker scorekeeper project, and I'll explain a problem that I have.
//Add point when player pots balls
buttons.yellowButton.addEventListener('click', () => {
coloredBall(2); // add 2 points to player
})
buttons.greenButton.addEventListener('click', () => {
coloredBall(3); // add 3 points to player
})
.
.
.
.
.
buttons.blackButton.addEventListener('click', () => {
coloredBall(7); // add 7 points to player
})
The code above works fine, It just updates the player score. Now, when all the reds are potted, I want to disable all of the buttons except the button that the players suppose to play. So I create a function that will add a new event to the buttons. The thing is that when I restart a game, I want to be able to remove those events, and to adds the same event when all the reds are potted again. How can I do this?
allRedArePotted = function() => {
buttons.yellowButton.disabled = false;
buttons.yellowButton.addEventListener('click', () => {
disableAllButtons();
buttons.greenButton.disabled = false;
yellow = 0;
})
buttons.greenButton.addEventListener('click', () => {
disableAllButtons();
buttons.brownButton.disabled = false;
green = 0;
})
buttons.brownButton.addEventListener('click', () => {
disableAllButtons();
buttons.blueButton.disabled = false;
brown = 0;
})
buttons.blueButton.addEventListener('click', () => {
disableAllButtons();
buttons.pinkButton.disabled = false;
blue = 0;
})
buttons.pinkButton.addEventListener('click', () => {
disableAllButtons();
buttons.blackButton.disabled = false;
pink = 0;
})
buttons.blackButton.addEventListener('click', () => {
black = 0;
checkWinner();
})
}
Use named functions (not anonymous) for event handlers and remove them anytime you want. For example:
// adding
document.addEventListener('click', clickHandler);
// removing
document.removeEventListener('click', clickHandler);
As mentioned in the comments, It is better to keep the state of your program somewhere and act according to that. Adding and Removing handlers is not a good approach.
someHandler(e) {
if(condition) {
// act1
}
else {
//act2
}
}

CSS class only applying to one element

I have some code that unpacks an array and returns an array of JSX elements. This works fine, however, when one element is clicked, the "selectedEl" css class is removed from all other elements.
I'm pretty sure I've just made some stupid mistake but I can't seem to figure it out. Thanks
Code that unpacks the array and assigns onClick method:
function UnpackReccArray() {
const renderArray = []
for (let renderEl = 0; renderEl < self.state.capMethod; renderEl++) {
renderArray.push(
<span className="topicElement" onClick={self.pushToChosen.bind(this, self.state.reccDataQuery[renderEl].topicID, renderEl + "topicEl")} id={renderEl + "topicEl"}>
<p className="fontLibre">{self.state.reccDataQuery[renderEl].displayName}</p>
</span>
)
if (renderEl + 1 === self.state.capMethod) {
return (
<self.ResultRender title="Popular Subjects" renderContent={renderArray} />
)
}
}
}
Code that handles onClick function
pushToChosen = (id, elID) => {
const self = this
const localChoseArray = this.state.subjectChosenArray
const index = localChoseArray.indexOf(id.toString())
if (index > -1) {
localChoseArray.splice(index, 1);
self.setState({
subjectChosenArray: localChoseArray
}, () => {
document.getElementById(elID).classList.remove("selectedEl")
})
} else {
self.setState({
subjectChosenArray: [...this.state.subjectChosenArray, id.toString()]
}, () => {
document.getElementById(elID).classList.add("selectedEl")
})
}
document.getElementById(elID).classList.toggle("selectedEl")
}
GIF of the code in action
Thanks!
i didn't fully understand the code but it seems that the toggle is what causing the issue since the if and else handles all possible cases , toggle is probably going to do the opposite of the if else bloc .

Make vanilla JS accordion auto close previous section

I found this great accordion with a very compact code but what I miss here is a function to auto close previous section when opened another one. It's probably quite easy but I'm a complete JS noob, sadly.
Original code source.
const items = document.querySelectorAll(".accordion a");
function toggleAccordion(){
this.classList.toggle('active');
this.nextElementSibling.classList.toggle('active');
}
items.forEach(item => item.addEventListener('click', toggleAccordion));
What have You tried so far ?
If You didn't, try the following logic.
Before you give an element a activeclass - loop over the rest of the elements and remove it from all of them :)
const items = document.querySelectorAll(".accordion a");
const remove = () => {
items.forEach(el => {
el.classList.remove('active');
el.nextElementSibling.classList.remove('active');
})
}
function toggleAccordion(){
remove()
this.classList.toggle('active');
this.nextElementSibling.classList.toggle('active');
}
items.forEach(item => item.addEventListener('click', toggleAccordion));
you could store the active one...
const items = document.querySelectorAll(".accordion a");
let active = null;
function toggleAccordion(){
if(active){
active.classList.toggle('active');
}
this.classList.toggle('active');
this.nextElementSibling.classList.toggle('active');
active = this;
}
items.forEach(item => item.addEventListener('click', toggleAccordion));

Categories