I'm making a pizza ordering application using pure javascript. In the CART section, I have two click operations - increase /decrease the item quantity and remove the item from cart. I have two event listeners for these two operations. I want to make use of event delegation here.
The DOM structure looks like this. Here the class cart-items is the parent class of all the items that are dynamically added into the card. Every item present in the cart gets a div and the ID associated with the pizza item chosen is assigned as ID for that item. For example, I add the pizza item with ID 4 in my cart, so a <div> with id 4 is added in the DOM. :-
Here are the event listeners for the increase/decrease in quantity and remove item from cart.
//event listener attached when remove button is clicked on
let removeCartItemButtons = document.getElementsByClassName("btn-danger");
for (let i = 0; i < removeCartItemButtons.length; i++) {
let button = removeCartItemButtons[i];
button.addEventListener("click", removeCartItem);
}
//event listener attatched when quantity is increased or decreased
let quantityInputs = document.getElementsByClassName("cart-quantity-input");
for (let i = 0; i < quantityInputs.length; i++) {
let input = quantityInputs[i];
input.addEventListener("change", quantityChanged);
}
MY TAKE ON:
document
.getElementsByClassName("cart-items")
.addEventListener()
);
I want to make use of event delegation and don't want to create multiple event listeners. So, I'm fetching the cart-items which is basically the parent element here for the CART section.
Now, how do I add the two event listeners inside the above code.
Please help!
for(const el of document.querySelectorAll(".cart-items")){
el.addEventListener('click', function(e){
if(e.target.matches('.btn-danger')){
removeCartItem(e.target);
}
});
el.addEventListener('change', function(e){
if(e.target.matches('.cart-quantity-input')){
quantityChanged(e.target);
}
});
}
Related
I'm working on making a checkbox appear when a user hovers over a table's first data cell.
I used "firstDataCell.onmouseover" to add the checkbox and "firstDataCell.onmouseout" to remove it which works as the checkbox appears but you are unable to click on the checkbox as the events propagate when you've hovered on it. I think the solution would be in some way to stop the other event propagation while one event is active but can't seem to work it out with ".stopPropagation()".
Code bellow:
let taskTable=document.querySelector('#table_1')
let tableBody=taskTable.getElementsByTagName('tbody')[0].getElementsByTagName('tr')
let tableRowArr=[]
for(let i=0;i<tableBody.length;i++){
tableRowArr.push(tableBody[i])
}
tableRowArr.forEach((row,index) => {
let tableRowFirstDataCell=row.getElementsByTagName("td")[0]
let cellData=tableRowFirstDataCell.innerHTML
tableCellsData.push(cellData)
tableRowFirstDataCell.onmouseover=addCheckboxes
})
function addCheckboxes(event){
event.currentTarget.onmouseover=()=>{}
event.currentTarget.innerHTML=""
let checkBox=document.createElement("input")
checkBox.setAttribute("type","checkbox")
checkBox.setAttribute("class","check")
// checkBox.setAttribute("") //<----- Add styling to checkboxes
// checkBox.addEventListener("onclick",deleteTask(id))
event.currentTarget.appendChild(checkBox)
event.currentTarget.onmouseout=removeCheckboxes
}
function removeCheckboxes(event) {
event.currentTarget.onmouseout=()=>{}
event.currentTarget.innerHTML=""
event.currentTarget.innerHTML=tableCellsData[1]
event.currentTarget.onmouseover=addCheckboxes
}
Basicly if I hover over a list item, I want to add a class to the corresponding span.
Now I've found how to do this with the following code.
My question: Is there a way to simplify this (without repeating)? If so, how exactly?
Edit
My first ever post here. Figured only giving this js would be sufficient.
So here is some more information.
This is about a navigation bar, which contains 4 list items. In every list item there is a span. If I hover over a particular listitem a border would apear on the corresponding span.
An eventListener for the whole page seems a bit rough, just want it for those 4 items.
var listItems = document.querySelectorAll(".hover");
var spanClass = document.querySelectorAll(".navbar-top-border");
listItems[0].addEventListener("mouseover", event => {
spanClass[0].classList.add("navbar-top-border-visible");
});
listItems[0].addEventListener("mouseout", event => {
spanClass[0].classList.remove("navbar-top-border-visible");
});
listItems[1].addEventListener("mouseover", event => {
spanClass[1].classList.add("navbar-top-border-visible");
});
listItems[1].addEventListener("mouseout", event => {
spanClass[1].classList.remove("navbar-top-border-visible");
});
Yes. Instead of biding each element to essentially the same event listeners, use "event delegation" where you bind the handler(s) to a common ancestor of the elements that need to use the callbacks. The event will originate at some element and then bubble up to the ancestor where it is handled. When it's handled, you can determine where it originated with event.target and then act accordingly.
Then, in your handler, if you need to access another element, use a DOM property to find that element in relation to the event.target (there are many possibilities to do this: closest, nextElementSibling, previousElementSibling, parent, etc.). Or, in your case, you can dynamically get the index of the moused over list item and act upon the span with that same index.
This way, you only set up handlers one time, which is less coding and less memory used by the various elements and no loops or hard-coded indexes are needed. It's also highly scalable as adding/removing DOM elements (either manually or dynamically) won't require any changes to the handler configurations.
Also, don't use .getElementsByClassName(), especially in connection with loops.
Here's an example:
// These collections will be used later to match up indexes
// but no looping or hard coding of indexes will be required.
var listItems = Array.from(document.querySelectorAll(".hover"));
var spanClass = document.querySelectorAll(".navbar-top-border");
// set up the event handler on a common ancestor
document.addEventListener("mouseover", foo1);
document.addEventListener("mouseout", foo2);
function foo1(event){
// Test whether the event originated at
// an element you care about
if(event.target.classList.contains("hover")){
// Find the span with the same index as the list item
// and add the desired class
spanClass[listItems.indexOf(event.target)].classList.add("navbar-top-border-visible");
}
}
function foo2(event){
// Test whether the event originated at
// an element you care about
if(event.target.classList.contains("hover")){
// Find the span with the same index as the list item
// and remove the desired class
spanClass[listItems.indexOf(event.target)].classList.remove("navbar-top-border-visible");
}
}
.hover { color:blue; text-decoration:underline; cursor:pointer; }
.navbar-top-border { display:none; }
.navbar-top-border-visible { display:inline; }
<ul>
<li class="hover">Item</li>
<li class="hover">Item</li>
<li class="hover">Item</li>
<li class="hover">Item</li>
</ul>
<span class="navbar-top-border">Item 1</span>
<span class="navbar-top-border">Item 2</span>
<span class="navbar-top-border">Item 3</span>
<span class="navbar-top-border">Item 4</span>
And how but this in case you really need only 0 and 1 as indexes.
var listItems = document.querySelectorAll(".hover");
var spanClass = document.querySelectorAll(".navbar-top-border");
let indxeses = [0, 1]
indxeses.forEach(el => {
listItems[el].addEventListener("mouseover", event => {
spanClass[el].classList.add("navbar-top-border-visible");
});
listItems[el].addEventListener("mouseout", event => {
spanClass[el].classList.remove("navbar-top-border-visible");
});
})
var listItems = document.querySelectorAll(".hover");
var spanClass = document.querySelectorAll(".navbar-top-border");
listItems.map(function(element) {
element.addEventListener("mouseover", event => {
spanClass.map(function(spanElement) {
spanElement.classList.add("navbar-top-border-visible");
});
});
element.addEventListener("mouseout", event => {
spanClass.map(function(spanElement) {
spanElement.classList.remove("navbar-top-border-visible");
});
});
});
You can loop through the items instead of using item indexes.
My goal is put javascript functions in loop:
select option from drop down
click on "Filter" button
download selected file
When i have it in two functions it works perfectly: - no loop, running manually (index from 0 until x)
function chooseOpt(x){
document.getElementsByClassName('span4 m-wrap dropdown combobox')[1].selectedIndex = x;
const links = Array.from(document.getElementsByClassName('btn green'));
links.forEach((link) => {
if (link.textContent === 'Filter') {
link.click();
}
})
}
function Dowload(){
var button = document.getElementsByClassName('btn icn-only black tooltips download_link')[0];
button.click();
}
But how can i put into loop?
i ve created one function from this two, but doesnt work:
function chooseOpt(){
for (x = 0; x<3;x++){
document.getElementsByClassName('span4 m-wrap dropdown combobox')[1].selectedIndex = x;
const links = Array.from(document.getElementsByClassName('btn green'));
var button = document.getElementsByClassName('btn icn-only black tooltips download_link')[0];
links.forEach((link) => {
if (link.textContent === 'Filter') {
link.click();
}
}
)
button.click();
}
}
chooseOpt();
Can you give me hint, link ? Thank you
Not seeing your HTML made it a bit challenging, but I contrived some markup that works with your script structure, then modified the script to successfully:
Loop through all options,
Simulate an associated link click for each option, and
Simulate a button click for each option to download an associated file
I encountered the same problem you did, and I was able to make it go away by adding a custom-event listener on the dropdown element. At least part of the issue for me was that programmatically changing the selectedIndex property of a select element does not trigger event listeners like calling the .click method on certain elements does. To make sure that the download function is called for each option, the code below employs a custom event (called programmatic-selection), which is triggered manually in the chooseOpt function.
Other things to note:
Each link has a data-file attribute that stores the path to the associated file. This value is copied to a global filepath variable when needed so the button can see which file to download.
Each link will be clicked if it has a class matching the current option's text content. (Your code selects links based on link.textContent === 'Filter', and this behavior is preserved: any link that matches the current option has its textContent property set to 'Filter' in order to meet your condition and thereby trigger a simulated click.)
// Declares global variables
const
dropdown = document.getElementsByClassName('dropdown')[1],
links = Array.from(document.getElementsByClassName('btn')),
button = document.getElementsByClassName('download-link')[0];
let filepath = '';
// Adds event listeners
dropdown.addEventListener('programmatic-selection', handleProgrammaticSelection);
document.addEventListener('click', handleLinkClick);
button.addEventListener('click', handleButtonClick);
// Main
chooseAll();
// Loops through options, selecting each one
function chooseAll(){
let i = -1;
while(++i < dropdown.length){
chooseOpt(i);
}
}
// Triggers custom event listener and the appropriate link
function chooseOpt(x){
dropdown.selectedIndex = x;
// The `change` event won't fire if we select an option
// programmatically, so we fire a custom event instead
dropdown.dispatchEvent(new CustomEvent('programmatic-selection'));
links.forEach((link) => {
if(link.textContent === 'Filter'){
// Simulates click on certain links, triggering listener
link.click();
}
});
}
// Listener for custom event -- sets link text
function handleProgrammaticSelection(event){
const dropdown = event.target;
links.forEach((link) => {
// `chooseOpt` relies on `textContent` property of `link` elements,
// so we set this before deciding which link to click
link.textContent = link.classList.contains(dropdown.value)
? 'Filter'
: 'Nope';
});
}
// (Listeners can automatically access their triggering events)
function handleLinkClick(event){
// An event has a `target` property
const clickedThing = event.target;
// Ignores irrelevant clicks
if(!clickedThing.classList.contains('btn')){ return; }
// Sets global `filepath` to match 'file' data-attribute of target
filepath = clickedThing.dataset.file;
// Calls `download`, which simulates button click
download();
}
// Called by above listener
function download(){
// Simulates click on `button`, triggering listener
button.click();
}
// Called by listener on `button`
function handleButtonClick(event){
// Accesses global `filepath` to pick file
console.log(`downloading ${filepath}...`);
}
<select class="dropdown">
<option>A</option>
<option>B</option>
</select><br/>
<select class="dropdown">
<option>AA</option>
<option>BB</option>
<option>CC</option>
</select><br/><br/>
<button data-file='some-file' class='btn AA'>Nope</button><br/>
<button data-file='some-other-file' class='btn BB'>Nope</button><br/>
<button data-file='another-file' class='btn CC'>Nope</button><br/><br/>
<button class='download-link'>Download</button>
Your question kind of doesn't make sense but, I think this is what you might want...
function chooseOpt(x){
document.getElementsByClassName('span4 m-wrap dropdown combobox')[1].selectedIndex = x;
const links = Array.from(document.getElementsByClassName('btn green'));
var button = document.getElementsByClassName('btn icn-only black tooltips download_link')[0];
links.forEach((link) => {
if (link.textContent === 'Filter') {
link.click();
}
});
button.click();
}
for(let x = 0; i < 3; x++){
chooseOpt(x);
}
I've made a simple todo list which so far has 2 functions. Checks off an item and deletes an item from a list.
I can delete all items if I start deleted from the bottom of the list but when I delete from the top, the first item gets deleted, the second item (now the first), deletes the item after it and then doesn't delete itself when I click on the X
I thought a stopPropagration() would help but doesn't seem to. It only stops the other checked function from running.
<ul>
<li><span>X</span> Code something</li>
<li><span>X</span> Wake up early</li>
<li><span>X</span> Buy popcorn</li>
</ul>
/**
* This strikes through list item on click once and item is marked as done
*/
const listItem = document.getElementsByTagName('li')
for (let i = 0; i < listItem.length; i++) {
listItem[i].addEventListener('click', () => {
listItem[i].classList.toggle('checked')
})
}
/**
* This deletes an item from the list on click
*/
const deleteItem = document.getElementsByTagName('span')
for (let i = 0; i < deleteItem.length; i++) {
deleteItem[i].addEventListener('click', e => {
deleteItem[i].parentNode.remove()
e.stopPropagation()
})
}
https://jsfiddle.net/k2u8mqes/
Expected result is that I should be able to delete each item, in any order
Your for loop is assigning some clicks to different elements from what you intend.
This suggestion uses the target of the click event to decide what needs to be deleted in real time. (Specifically, it removes the li that is the parent of the clicked span.)
document.addEventListener("click", checkLi);
document.addEventListener("click", deleteSpanParent);
function checkLi(event){
if(event.target.tagName == "LI"){
event.target.classList.toggle("checked");
event.target.style.color = "grey";
}
}
function deleteSpanParent(event){
if(event.target.tagName == "SPAN"){
let span = event.target, li = span.parentNode, ul = li.parentNode;
ul.removeChild(li);
}
}
<ul>
<li><span>X</span> Code something</li>
<li><span>X</span> Wake up early</li>
<li><span>X</span> Buy popcorn</li>
</ul>
You are modifying an array while iterating it, which is a common caveat.
When you delete from top:
deletes the 1st item, calls deleteItem[0].parentNode.remove(), removes the 1st element of deleteItem array, it's okay;
deletes the 2nd item, calls deleteItem[1].parentNode.remove(), but the deleteItem is now of size 2, you are actually deleting the
3rd element of the original array;
deletes the 3rd item, calls deleteItem[2].parentNode.remove(), but the deleteItem is now of size 1, you are running out of index;
Working snippet:
for (let i = 0; i < deleteItem.length; i++) {
deleteItem[i].addEventListener('click', e => {
e.target.parentNode.remove()
})
}
The problem is where the way you have used for loops to bind event listeners and how you're using accessing the particular element which the event bound to.
Instead of doing this;
listItem[i].classList.toggle('checked')
//and
deleteItem[i].parentNode.remove()
You can use the event listener's event object to access to bound element, like this;
e.target.classList.toggle('checked')
//and
e.target.parentNode.remove()
Please refer to the event listener API here: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
check this fiddle for a working example of your code : https://jsfiddle.net/491rLbxj/
I am quite new to manipulating elements in the DOM in JS so I am creating a simple to do list to get more comfortable and where I can add items using the input and remove items by clicking on the list item.
ALthough this may not be best practice and limitting I am just wanting to use create and remove elements rather than using objects or classes until I get more familar, also using plain/vanilla js so please keep this in mind when answering.
I am trying to add a click event which removes the <li> when the <li> is clicked.
My logic is...
When the page is loaded I can't just run a for loop over all of the <li>s and add event handlers as all of the <li>'s do not exist yet.
So my attempted solution is when the addTaskButton event is triggered, we get all of the <li> that are on the page at the time of the event, we loop through all of them and add an eventlistener to <li>'s that are waiting to be removed when clicked.
This doesn't seem to work and may be overly complicated.
Can someone please explan to me very simply like I'm 5 why this doesn't work or what a better way to do this would be?
Thank you in advance
HTML
<ul id="taskList">
<li>example</li>
</ul>
<input type="text" id="addTaskInput">
<button id="addTaskButton">Add Task</button>
JavaScript
const taskList = document.querySelector("#taskList");
const addTaskInput = document.querySelector("#addTaskInput");
const addTaskButton = document.querySelector("#addTaskButton");
let taskItem = document.querySelectorAll("li");
addTaskButton.addEventListener("click", () => {
let taskItem = document.createElement("li");
taskItem.textContent = addTaskInput.value;
for (let i = 0; i < taskItem.length; i++) {
taskItem[i].addEventListener("click", () => {
let taskItem = document.querySelectorAll("li");
taskList.removeChild(taskItem[i]);
});
}
taskList.appendChild(taskItem);
addTaskInput.value = " ";
});
Here is code i created for your requirement, this implement jQuery $(document).on mechanism in vanilla javascript, now where ever you create an li inside the document, on clicking that li it will be removed.
Explaination
What it does is on clicking the document it checks on which element is clicked (e.target is the clicked element, e is is the click event on document), then checks if the clicked item is an li tag (e.target.tagName will tell us the tag name if the item clicked), so if it is an li just remove it;
const taskList = document.querySelector("#taskList");
const addTaskInput = document.querySelector("#addTaskInput");
const addTaskButton = document.querySelector("#addTaskButton");
addTaskButton.addEventListener("click", () => {
let taskItem = document.createElement("li");
taskItem.textContent = addTaskInput.value;
taskList.appendChild(taskItem);
addTaskInput.value = " ";
});
document.onclick = function(e)
{
if(e.target.tagName == 'LI'){
e.target.remove();
}
}
<ul id="taskList">
<li>example</li>
</ul>
<input type="text" id="addTaskInput">
<button id="addTaskButton">Add Task</button>
Update your for loop like so:
for (let i = 0; i < taskItems.length; i++) {
taskItems[i].addEventListener("click", () =>
taskList.removeChild(taskItems[i]);
});
}
Also your initial taskItem variable should be taskItems and is reflected in the for loop above.
taskList.addEventListener("click", (event) => {
event.target.remove();
});
When the specified event occurs the event object is returned.
The event object has several properties, one of them being target which is the element which is the element which the event occured on. event.target is returned to us and we are applying the remove() method to event.target
because of event "bubbling" or "Event Propagation", we can attach the event handler to an ancestor. It's best to attach the event listener to the closest ancestor element that is always going to be in the DOM (won't be removed).
When an event is triggered-in this case the "click" event. All decending elements will be removed - which in our case as there are only <li>'s this would be fine. But we should be more specific as in a different case we could be attaching this event handler to a div which has several different elements.
To do this we add an if condition to check that the tagName is an <li>
if (event.target.tagName == "LI")
note that the element must be calpitalised
Solution is as follows
taskList.addEventListener("click", (event) => {
if(event.target.tagName == "LI"){
event.target.remove();
}});
Further reading:
Event object and its properties:
https://developer.mozilla.org/en-US/docs/Web/API/Event
Event Bubbling:
https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles
tagName:
https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName