I am trying to make a drop down list that dynamically adds elements from an API. When the user selects an item in the dropdown, it should add a class called "current" to that item. Only one dropdown item in the list can have the class 'current' applied to it.
I have successfully create the HTML elements (listItem) and appended them to the list. However, when I try to add event listener, the event registers with the child elements img and div with text such that when user clicks those, the class 'current' is applied there and not the parent node.
I read up on "event bubbling" but not sure if this is my issue or not.
document.addEventListener('DOMContentLoaded', async () => {
const dropDownToggle = document.querySelector('.w-dropdown-toggle')
const dropDownList = document.querySelector('.w-dropdown-list')
const countries = await getCountries();
countries.forEach((country) => {
const countryName = country.name.common;
const cca2 = country.cca2;
const svgUrl = country.flags.svg;
let prefix = country.idd.root + country.idd.suffixes?.[0]
prefix = Number.isNaN(prefix) ? prefix="" : prefix
//console.log(countryName, cca2, prefix)
const listItem = createListItem(countryName, cca2, svgUrl, prefix);
// Bad code here: <a> tag gets event listener but so do its children
listItem.addEventListener("click", (event) => {
console.log(event);
//console.log(event);
//document.querySelector('current')?.classList.remove('current');
//document.querySelector('current').ariaSelected = false;
console.log('hello')
event.target.classList.add("current");
event.target.ariaSelected = true;
console.log('goodbye')
});
dropDownList.append(listItem);
})
});
const getCountries = async () => {
let url = 'https://restcountries.com/v3.1/all'
const response = await fetch(url)
const data = await response.json();
return data;
}
const createListItem = (countryName, cca2, svgUrl, prefix) => {
const template = `
<a data-element="item" aria-role="option" aria-selected="false" href="#" class="dropdown_item w-inline-block" tabindex="0"><img src="${svgUrl}" loading="lazy" data-element="flag" alt="" class="dropdown_flag"><div data-element="value" class="dropdown_text">${cca2}</div></a>
`
const listItem = document.createElement("template");
listItem.innerHTML = template.trim();
return listItem.content.firstElementChild
}
This is expected. event.target is the innermost element that was actually clicked, not the one that the event listener is installed on.
To use the latter, just refer to event.currentTarget or listItem, instead of event.target.
Related
i'm trying to create a custom pupop in javascript, this is my first time with this.
I have a problem with the close button, the "x" target correctly the div to close, but doesn't remove the "active" class at click.
https://demomadeingenesi.it/demo-cedolino/
HTML CODE
<div class="spot spot-2">
<div class="pin"></div>
<div class="contenuto-spot flex flex-col gap-3">
<img class="chiudi-popup" src="img/chiudi.svg" />
[---CONTENT---]
</div>
</div>
</div>
JAVASCRIPT CODE
const tooltips = function () {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
const chiudiPopup = document.querySelector(".chiudi-popup");
spot.classList.add("active");
contenuto.classList.add("show");
if (curActive && curActive !== spot) {
curActive.classList.toggle("active");
contActive.classList.toggle("show");
}
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
});
});
const chiudiPopup = document.querySelector(".chiudi-popup");
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
What the code above does is adding an click listener, but it's inside another click listener, so all it's doing is adding an click listener on the first .chiudi-popup that removes .active and .show from the last spot element.
It's hard to see if this is correct, because you haven't given us enough to reproduce the problem, but I moved the code above outside the spot.addEventListener("click", () => { and instead of searching the whole document with const chiudiPopup = document.querySelector(".chiudi-popup"); the code nows only targets the .chuidi-popup element within the spot: const chiudiPopup = spot.querySelector(".chiudi-popup");
const tooltips = function() {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
spot.classList.add("active");
contenuto.classList.add("show");
if (curActive && curActive !== spot) {
curActive.classList.toggle("active");
contActive.classList.toggle("show");
}
});
// MOVED FROM THE CLICK LISTENER
const chiudiPopup = spot.querySelector(".chiudi-popup");
chiudiPopup.addEventListener("click", () => {
spot.classList.remove("active");
contenuto.classList.remove("show");
});
});
EDIT: I missed that you have the img.chiudi-popup inside your container, which will trigger both event listeners. I would honestly just simplify the code and always hide the container when clicking on it again. You can still have the img.chiudi-popup (close image) to make it easier for the users to understand that they can click on it.
const tooltips = function() {
const spots = document.querySelectorAll(".spot");
spots.forEach((spot) => {
const contenuto = spot.querySelector(".contenuto-spot");
const pin = spot.querySelector(".pin");
spot.addEventListener("click", () => {
let curActive = document.querySelector(".spot.active");
let contActive = document.querySelector(".contenuto-spot.show");
if (curActive !== spot) {
spot.classList.add("active");
contenuto.classList.add("show");
}
if (curActive) {
curActive.classList.remove("active");
contActive.classList.remove("show");
}
});
I have a code that adds or removes a class from elements. But it does this only for the menu, but not for the content (a huge white square in the middle), although it should for both at the same time. I need that when clicking on any button in the menu, the 'is-active' class changes both in the menu and in the content.
without clicking on the menu.png
with a click on the menu.png
const list = Array.from(document.querySelectorAll('.list'))
const play = Array.from(document.querySelectorAll('.play'))
const clearActiveClass = (element, className = 'is-active') => {
element.find(item => item.classList.remove(`${ className }`))
}
const setActiveClass = (element, index, className = 'is-active') => {
element[index].classList.add(`${ className }`)
}
const checkoutTabs = (item, index) => {
item.addEventListener('click', () => {
if (item.classList.contains('is-active')) return
console.log(item)
clearActiveClass(list)
clearActiveClass(play)
setActiveClass(list, index)
setActiveClass(play, index)
})
}
list.forEach(checkoutTabs)
The inner function to find needs to be something that finds an element containing className. Once you have found the element, you can remove the className class.
const clearActiveClass = (element, className = 'is-active') => {
element.find(item => item.classList.contains(className)).classList.remove(className);
}
I am writing a function for autofill, which will display a list of values to select as potential value input. The names of the input fields are dynamic so I need to pass that to the function that is executed by the event handler, but I also need to prevent default button action. The error that I've been getting is that the passed targetField gets seen as the event and I have not been able to figure out how to fix this. Would appreciate some help. This is what I have now.
function dropdownList(testlist, clicked) {
const listEl = document.createElement("ul");
listEl.classname = "autocomplete-list";
testlist.forEach(item => {
const listItem = document.createElement("li");
const valueButton = document.createElement("button");
valueButton.innerHTML = item
valueButton.addEventListener("click", valueSelect(targetField);
listItem.appendChild(valueButton);
listEl.appendChild(listItem)
})
let clickedId = `scripts-${clicked}-script`
let targetField = document.querySelector("[data-testid=" + clickedId + "]")
document.querySelector("[data-testid=" + clickedId + "]").after(listEl)
valueSelect(targetField);
}
function valueSelect(targetField) {
event.preventDefault();
const buttonEl = event.target;
targetField.value = buttonEl.innerHTML
}
This code works. Your problem was mostly with the eventListener, and with the parameters of the valueSelect function.
Note: I changed your clicked variable to be the actual element, not the element's id
document.querySelector("#myInput").addEventListener("click", (event)=>dropdownList(["a", "b", "c"], event.target))
function dropdownList(testlist, clicked) {
const listEl = document.createElement("ul");
listEl.classname = "autocomplete-list";
testlist.forEach(item => {
const listItem = document.createElement("li");
const valueButton = document.createElement("button");
valueButton.innerHTML = item
valueButton.addEventListener("click", (event)=>valueSelect(event, clicked));
listItem.appendChild(valueButton);
listEl.appendChild(listItem)
})
clicked.after(listEl)
}
function valueSelect(event, targetField) {
event.preventDefault();
const buttonEl = event.target;
targetField.value = buttonEl.innerHTML
}
<label>Test</label><input type="text" id="myInput" />
After i get data from the api, I want to change it's margin. But it's not originally in the html, I'm thinking about event propagation but I couldn't do even if i research.
It's how i get the data
const getImages = async function () {
const res = await fetch("https://jsonplaceholder.typicode.com/photos");
const data = await res.json();
data.map((img) => {
const markup = `
<img
src="${img.url}"
alt="Image"
title="${img.title}"
width="100%"
/>
`;
imgContainer.insertAdjacentHTML("afterbegin", markup);
});
};
I tried event propagation but i couldn't think a proper event for this
const propagation = function () {
imgContainer.addEventListener("load", function (e) {
console.log(e.target);
});
};
Could you just inline the style in the markup var? Or add a class attribute with the margin you want set?
On the site I have a navbar. When I click on the a in navbar I want a new div (with content) appears and everything else "hides". I did it withdisplay: none. In the corner there should be an "X" which closes the div and makes "first screen" appear again.
I did the first part, but have some problems with closing. Here is my code:
const about = document.getElementById('li-about');
const work = document.getElementById('li-work');
const contact = document.getElementById('li-contact');
^These are links in a navbar.
const openSlide = (id) => {
document.querySelector('main article#' + id).style.display = 'block';
document.querySelector('header').style.display = 'none';
};
about.addEventListener('click', () => openSlide('about'));
work.addEventListener('click', () => openSlide('work'));
contact.addEventListener('click', () => openSlide('contact'));
And here is the code. It works. But when I want to do similar thing to a closing div function, it only works with about, not work and contact.
const closeSlide = (id) => {
document.querySelector('main article#' + id).style.display = 'none';
document.querySelector('header').style.display = 'block';
};
const close = document.querySelector('.close');
close.addEventListener('click', () => closeSlide('about'));
close.addEventListener('click', () => closeSlide('work'));
close.addEventListener('click', () => closeSlide('contact'));
Can you tell me why? There is no error in console, I tried to replace closeSlide function with a simple console.log and it doesn't work as well. Is it possible that JavaScript detects only one (first in HTML code) .close div?
Is it possible that JavaScript detects only one (first in HTML code) .close div?
Yes, document.querySelector returns the first matching element it finds in the DOM. If you want to add your listeners to every .close on the page, either loop through the NodeList returned by document.querySelectorAll:
const closeList = document.querySelectorAll('.close');
closeList.forEach(element => {
element.addEventListener('click', () => closeSlide('about'));
element.addEventListener('click', () => closeSlide('work'));
element.addEventListener('click', () => closeSlide('contact'));
};
or add a listener to an element containing all of your .close elements that only takes action if a .close was clicked:
document.querySelector('#some-container').addEventListener('click', evt => {
if (!evt.target.closest('.close')) return;
closeSlide('about');
closeSlide('work');
closeSlide('contact');
});