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.
Related
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
}
}
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
What I am trying to accomplish is making the clicked tab active
I have seen a lot of jQuery examples, but I'm using JavaScript ES6
I know it's an easy task but my mind is so done right now that I can't think
here is the code: https://codepen.io/revatto/pen/wvMzOqo
here is my main.js:
const tabs = document.querySelectorAll('[data-tab-target]');
const tabsContents = document.querySelectorAll('[data-tab-content]');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = document.querySelector(tab.dataset.tabTarget);
tabsContents.forEach(tabContent => {
tabContent.classList.remove('active');
})
target.classList.add('active');
})
})
You were only adding and removing 'active' class from tab contents you need to do the same with 'li' i.e. list too.
const tabs = document.querySelectorAll('[data-tab-target]');
const tabsContents = document.querySelectorAll('[data-tab-content]');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const target = document.querySelector(tab.dataset.tabTarget);
tabsContents.forEach(tabContent => {
tabContent.classList.remove('active');
})
target.classList.add('active');
//loop through 'li' items and remove 'active' class
tabs.forEach(tab => {
tab.classList.remove('active');
})
//add 'active' class to clicked 'li' item
e.target.classList.add("active");
})
})
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));
This is kind of tricky, but I need to make the large code block below generic.
I have a number of views that follow the same format: A table full of rows, where each row contains a delete icon. Each icon has a data-id attribute which is the item's _id in the database.
I am wiring the click action on each icon to where it opens a standard dialog to ask for confirmation. As you can guess, the "Yes" button of the dialog will have an onclick which calls the desired function with a parameter of the item's _id. e.g., setting onclick = deleteContact(fdke75jdsgtd7i)
Let's say I have 3 tables: Contacts, Cases and Firms.
I got all the wiring to work for any given table, provided I copy and paste the following code block in every view with the caveat that I have to use the commented onclick line instead of the generic, uncommented line below it.
let deleteItemAnchors = document.getElementsByClassName("delete-item");
Array.from(deleteItemAnchors).forEach( (item) => {
item.addEventListener('click', () => {
// Highlight the selected row.
highlightedTableRow = item.closest("tr");
highlightedTableRow.classList.add("table-warning");
// The record's _id is in the data-id attribute.
let itemId = item.getAttribute("data-id");
let buttons = [{
//onclick: () => { removeTableRowHighlight(); deleteContact(itemId); },
onclick: () => { removeTableRowHighlight(); deleteFunction(itemId); },
text: "Yes"
}, {
onclick: () => { removeTableRowHighlight(); },
text: "No",
class: "btn-secondary"
}];
let confirmDelete = new CustomDialog("Delete this " + recordType + "?", 'Click "Yes" to delete it. Press "No" to cancel.', buttons);
});
});
Here is an example of the function called when the button is clicked:
function deleteContact(itemId) {
console.log("You deleted the item with id = " + itemId);
}
Bear in mind, it is only working if the commented onclick line is live, and the generic line right below it is comment out.
I want to stop recreating the big block of code for every view by moving it to a re-usable function which can be called from each of the 3 views as follows:
let deleteFunction = () => { deleteContact(); };
wireDeleteIcons("Contact", deleteFunction);
let deleteFunction = () => { deleteCase(); };
wireDeleteIcons("Case", deleteFunction);
So, I moved the code block to a function called, "wireDeleteIcons," which accepts:
the record type as a string, and
the function which does the deleting, like deleteCase() or deleteContact().
From my Contacts view, I am calling:
let deleteFunction = () => { deleteContact(); };
wireDeleteIcons("Contact", deleteFunction);
It is all working so far, except for the deleteFunction(itemId) call when the icon is clicked.
If you look back up at the big code block, check out the line below the commented onclick line.
I am trying to add the parameter, itemId, to the function that was passed. On testing, it makes it all the way to my deleteContact() function, but it doesn't pass in the _id. So, my console.log shows, per my deleteContact() function, "You deleted the item with id = undefined"
How can I pass the function generically, and insert the parameter into it from within my generic wireDeleteIcons() function?
Your deleteFunction() needs to take an argument:
let deleteFunction = (id) => deleteContact(id);
But you don't really need the deleteFunction variable. Just write:
wireDeleteIcons("Contact", deleteContact);
wireDeleteIcons("Case", deleteCase);
The definition of wireDeleteIcons should be something like this:
function wireDeleteIcons(tableId, deleteFunction) {
let deleteItemAnchors = document.getElementById(tableId).getElementsByClassName("delete-item");
Array.from(deleteItemAnchors).forEach( (item) => {
item.addEventListener('click', () => {
// Highlight the selected row.
highlightedTableRow = item.closest("tr");
highlightedTableRow.classList.add("table-warning");
// The record's _id is in the data-id attribute.
let itemId = this.getAttribute("data-id");
let buttons = [{
onclick: () => { removeTableRowHighlight(); deleteFunction(itemId); },
text: "Yes"
}, {
onclick: () => { removeTableRowHighlight(); },
text: "No",
class: "btn-secondary"
}];
let confirmDelete = new CustomDialog("Delete this " + recordType + "?", 'Click "Yes" to delete it. Press "No" to cancel.', buttons);
});
});
}