I'm making a Rock Paper Scissors Game to practice DOM manipulation and I have to select a button in JavaScript to know user's choice.
I created an event listener to each button but somehow when the event fires it's target is one of the button's children nodes (in my search I discovered that this is related to event bubbling I think).
I tried to use capture, but it's not working too.
So I'm trying to use a recursive function to grab the parent node until it reaches the button itself. But in the console, when it actually reaches the button node, it returns undefined and doesn't attribute to buttonChecked.
let buttons = document.querySelectorAll('.game__button')
buttons.forEach(button => button.addEventListener('mousedown', rockPaperScissors, {capture:true}));
function checkValidOption(element){
if(element.hasAttribute('data-option')){
console.log(element.nodeName);
return element;
}
else {
console.log("Not Found it")
element = element.parentNode;
checkValidOption(element);
}
}
function rockPaperScissors(event) {
let buttonPressed = event.target;
let buttonChecked = checkValidOption(buttonPressed);
console.log(buttonChecked);
// A lot of comments below
}
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="images/icons8-rock-80.png" alt="">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
Try event.currentTarget to get the component the listener was added to, rather than an internal element.
See: Event.currentTarget
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.
The event.target could be the button, or any child within the button. If we only want the button, no matter where it is clicked, reference the event's current target (the owner of the event).
let buttonPressed = event.currentTarget; // Only need to change this line
let buttons = document.querySelectorAll('.game__button')
buttons.forEach(button =>
button.addEventListener('mousedown', rockPaperScissors, { capture: true }));
function checkValidOption(element) {
if (element.hasAttribute('data-option')) {
console.log(element.nodeName);
return element;
} else {
console.log("Not Found it")
element = element.parentNode;
checkValidOption(element);
}
}
function rockPaperScissors(event) {
let buttonPressed = event.currentTarget;
let buttonChecked = checkValidOption(buttonPressed);
console.log(buttonChecked);
// A lot of comments below
}
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="images/icons8-rock-80.png" alt="">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
All you need to do is set the event up on the button elements and then use this to refer to the button that gets clicked which triggers the handler. From there, you can access anything about that object (including its nested content).
// The click event is more appropriate than mousedown for buttons.
document.querySelectorAll('.game__button').forEach(button => button.addEventListener('click', rockPaperScissors));
function rockPaperScissors(event) {
// In a DOM event handler, "this" will reference the
// DOM object that is handling the event, not necessarially
// the object that triggered the event (which is event.target).
console.log(this);
console.log("You clicked: " + this.querySelector("figcaption").textContent);
}
img { height: 50px; }
<button class="game__button" data-option="1">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://images.thdstatic.com/productImages/94af8836-0338-4802-914e-04cc71e562ad/svn/backyard-x-scapes-fake-rocks-hdd-rof-rocsb-64_1000.jpg" alt="rock">
</div>
<figcaption class="game__figure--figcaption">Rock</figcaption>
</figure>
</button>
<button class="game__button" data-option="2">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://upload.wikimedia.org/wikipedia/commons/5/52/Continuous_form_paper_%2814p875_x_11%29.jpg" alt="paper">
</div>
<figcaption class="game__figure--figcaption">Paper</figcaption>
</figure>
</button>
<button class="game__button" data-option="3">
<figure class="game__figure">
<div>
<img class="game__figure--img" src="https://cdn.thewirecutter.com/wp-content/uploads/2015/03/scissors-kitchen-shears-lowres.jpg" alt="scissors">
</div>
<figcaption class="game__figure--figcaption">Scissors</figcaption>
</figure>
</button>
Well you just need to store the value of recursion in a variable. As simple as that.
Like this in else block -:
console.log("Not Found it");
element = element.parentNode;
let buttonElement = checkValidOption(element);
return buttonElement;
Full function -:
function checkValidOption(element) {
if (element.hasAttribute("data-option")) {
console.log(element.nodeName);
return element;
} else {
console.log("Not Found it");
element = element.parentNode;
let buttonElement = checkValidOption(element);
return buttonElement;
}
}
Related
as I said in title I have problem with HTML elements created with Element.insertAdjacentHTML() method, I'm trying about an hour to solve this but can't. I have button that create new HTML elements, couple of that elements is new buttons with same class or id, it's no matter, that I need to catch in some variable and than again use for event listener, for some reason the class or id for these new created button doesn't exist, is there any way to catch it and use it later, I need Vanila Javascript?
There is over 500 lines of code, this is only for mentioned method
btnClaim.addEventListener("click", () => {
rewardCurrent.style.display = "none";
claimedRewards.push(currentReward);
rewardsList.innerHTML = ``;
claimedRewards.forEach(function (rew, i) {
const html = `
<div class="reward" id="${i}">
<div class="img-text-cont">
<img src="${rew.imgUrl}" alt="">
<div class="text-cont">
<p class="claimed-reward-title">${rew.title}</p>
<p class="claimed-reward-price">$${rew.price}</p>
</div>
</div>
<div class="claimed-rewards-action">
<button id="btn-sell2">Sell</button>
<button id="btn-ship">Ship</button>
</div>
</div>
`;
rewardsList.insertAdjacentHTML("afterbegin", html);
I need that btn-sell2 and btn-ship buttons in variables.
your element is going to be created and doesn't exist at the time page loads, so js addeventlistener will throw an error. to solve you have 2 ways.
1- use parent node that element will be created inside.
addevenlistener to parent and use
parent.addeventlistener( event, function (event){
if(event.target.classList.contains("childClass") {}
}
2- give addeventlistener when creating the element :
function createElement () {
const elem = -craete elemnt-
elem.addeventlistener(event, function);
}
In the following code:
<div onClick={() => console.log('outer div')}>
<div onClick={() => console.log('middle div')}>
<div onClick={() => console.log('innermost div')}>
Click me!
</div>
</div>
</div>
I understand that the event object possesses a target property that contains a reference to the DOM node on which the event occurred. My question is: in the capturing (when capture set to true) and bubbling phases, will the event always fire on an HTML element if that element contains the same HTML attribute (ex. onClick, onHover) that started the propogation cycle? Thanks.
YES.
The event always fires on an HTML element if that element contains the same HTML attribute.
In the code below, you can see which phase of the event fires on a specific node.
Event.eventPhase interface indicates which phase of the event flow is currently being evaluated.
0: none, 1: capturing, 2: target, 3: bubbling
// true: capturing / false: bubbling
const useCature = true;
const phase = (e) => {
const phases = ["capturing", "target", "bubbling"];
const node = `${e.currentTarget.nodeName} .${e.currentTarget.className}`;
console.log(node, phases[e.eventPhase - 1]);
};
document.querySelector(".outer").addEventListener("click", phase, useCature);
document.querySelector(".middle").addEventListener("click", phase, useCature);
document.querySelector(".inner").addEventListener("click", phase, useCature);
<div class="outer">
<div class="middle">
<div class="inner">
Click me!
</div>
</div>
</div>
I'm trying to get the closest ancestor element to a clicked element, from an array of provided elements. When a user clicks on a tab, I need to find out where on the page that tab is.
I have something kind of working, however this script thinks main is the closest tag, when it should be article.
If I put article before main in the array it works just fine, however I want this to work regardless of array order. Any ideas?
// Tab Click
var tabs = Array.from(document.querySelectorAll('details.company-details'));
function handleTabClick(e) {
var tabNode = e.target;
tabLabel = tabNode.innerText;
const tgt = e.target;
var location;
var elements = ['section', 'header', 'main', 'nav', 'article'];
for (let element of elements) {
if (tgt.closest(element)) {
location = element;
break;
};
}
console.log(location);
};
tabs.forEach(function(tab) {
tab.addEventListener('click', handleTabClick);
});
<main class="company-main-wrapper" id="maincontent">
<article>
<div class="company-grid-row">
<div class="company-grid-column-two-thirds">
<div class="company-page-content">
<div class="block-expander">
<details class="company-details company-expander" company-polyfilled="true" id="company-details0" open="">
<summary class="company-details__summary" role="button" aria-controls="company-details__text0" tabindex="0" aria-expanded="true">
<span class="company-details__summary-text">
sampoe page
</span>
</summary>
</details>
</div>
</div>
</div>
</div>
</article>
</main>
See the comments inline below:
// Don't need Array.from() because the node list returned from
// .querySelectorAll() supports .forEach().
document.querySelectorAll('details.company-details').forEach(function(tab) {
tab.addEventListener('click', handleTabClick);
});
var elements = ['section', 'header', 'main', 'nav', 'article'];
let found = false;
let parent = null;
function handleTabClick(e) {
// Start at the parent of the tab
parent = this.parentNode;
// Keep looping until we find an ancestor that matches
while (found === false){
// Check the name of the parent element to see if its lower
// cased name is in the array.
if(elements.includes(parent.nodeName.toLowerCase())){
found = true; // Found a match
} else {
// No match. Get the next parent and keep looping
parent = getParent(parent);
}
}
console.log(parent);
};
function getParent(element){
return element.parentNode;
}
<main class="company-main-wrapper" id="maincontent">
<article>
<div class="company-grid-row">
<div class="company-grid-column-two-thirds">
<div class="company-page-content">
<div class="block-expander">
<details class="company-details company-expander" company-polyfilled="true" id="company-details0" open="">
<summary class="company-details__summary" role="button" aria-controls="company-details__text0" tabindex="0" aria-expanded="true">
<span class="company-details__summary-text">
sampoe page
</span>
</summary>
</details>
</div>
</div>
</div>
</div>
</article>
</main>
I have DOM elements as shown below. I want to sort it on the basis of href attribute.
This is what I have tried in JS but more need to be done.
document.addEventListener("DOMContentLoaded", function () {
let elems = Array.from(document.querySelectorAll(".house-senate a"));
elems.sort((a, b) => a.textContent.localeCompare(b.textContent));
});
Problem Statement:
I am wondering what JS code I need to add so that it sorts everything on the basis of href attributes.
You're close, but:
You need to actually move them in the DOM.
You're potentially sorting ones that aren't in the same parent (though they all are in your example HTML).
blex pointed out to me that you want to sort by the category in the href, not by the href itself. In your example, it comes to the same thing because the text prior to the category in all the hrefs is the same, but still, perhaps better to extract it.
This is blex's function for extracting it:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
Or if you want to be more rigorous about extracting that parameter from the query string, this collaborative answer originally by Code Spy shows how to do that.
See comments for more:
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
If you need to add more sorting criteria (per your comment under the question), just add them in the sort callback; this question has answers showing how to sort an array of objects on multiple criteria.
I've assumed above that there aren't hundreds of these links. If there are, and if you see a performance problem with the above, you can remove the container from the DOM before moving the links around within it, then put it back:
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Remember its parent and following sibling and remove it
const parent = container.parentNode;
const sibling = container.nextSibling;
parent.removeChild(container);
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
// Put the container back -- note this works even if the
// container was the last child in the parent
// and `sibling` is `null`.
parent.insertBefore(container, sibling);
});
<div>This is before the <code>div</code> with the links in it.</div>
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
<div>This is after the <code>div</code> with the links in it.</div>
Note: You're using modern language features, but the above relies on a modern browser feature (NodeList being iterable). If you're transpiling, it may not be that all the browsers you're targeting have the necessary feature, but for anything even vaguely modern, you can polyfill it; see my answer here for details.
I'm trying to add an event listener on some repeating innerHTML. I.E for every lot of HTML added by innerHTML, I'll also need to add a click event onto it.
To complicate things I'm also importing a data set from another JS file imported under the name data. As you can see in the code I need the data inside the event listener to be specific to the for loop iteration of the innerHTML so that when I fire the event listener I can see the correct, corresponding data.
This is my attempt:
JS:
import data from './data.js';
import img from './images.js';
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
for (let i = 0; i < data.lists.length; i++) {
let obj = eval(data.lists[i]);
let totalReminders = getTotalReminders(obj);
lists.innerHTML += `
<div class="flex-between row list">
<h4>${obj.name}</h4>
<div class="flex-between sections">
<h4 class="number-width flex-c">${obj.items.length}</h4>
<h4 class="number-width flex-c">${totalReminders}</h4>
<div class="text-width flex-end">
<h4 class="date">${obj.created}</h4>
<img class="img-radius" src="${img.symbols[3]}" alt="Delete Bin">
</div>
</div>
</div>
`;
const list = document.querySelector('.list');
list.addEventListener('click', () => { // click event
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
};
};
const getTotalReminders = passed => { // find total reminders
let total = 0;
for (let i = 0; i < passed.items.length; i++) {
total += passed.items[i].reminders;
};
return total;
};
At the moment ONLY the first iteration of innerHTML += has an event listener attached and when I click on it I see the data that should be corresponding the last iteration.
What am I doing wrong here?
You need to move the code that sets up the event handlers so that it is outside of your for loop and runs after that loop is finished. Then, instead of .querySelector(), which only returns the first matching element, you need .querySelectorAll() to return all matching elements. After that, you'll loop through all those elements and set up the handler.
You'll also need to change how your obj variable is declared so that it will be in scope outside of the for loop. Do this by declaring it just before the loop, but assigning it inside the loop:
let obj = null; // Now, obj is scoped so it can be accessed outside of the loop
for (let i = 0; i < data.lists.length; i++) {
obj = eval(data.lists[i]);
And, put the following just after the for loop finishes:
// Get all the .list elements into an Array
const list = Array.prototype.slice.call(document.querySelectorAll('.list'));
// Loop over the array and assign an event handler to each array item:
list.forEach(function(item){
item.addEventListener('click', () => {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
});
With all this said, your approach here is really not very good. There is almost always another option than to use eval() for anything and using .innerHTML is usually something to avoid due to its security and performance implications. Using it in a loop is almost always a bad idea. You really should be using the DOM API to create new elements, configure them and inject them into the DOM. If you must use .innerHTML, then build up a string in your loop and after the loop, inject the string into the DOM via .innerHTML, just once.
One options is to look at event delegation/bubbling. The basic principle here is you add the event handler to a parent object, in this case <div id="lists"></div>. Then when the event is fired you query the target of that event to see if it matches your element.
Using this technique you don't have to re-bind event handlers when new items are added, particularly useful if the items are added by user interaction.
In your case it would look something like:
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
//Now that the parent element is added to the DOM
//Add the event handler
lists.addEventListener("click",function(e) {
// e.target was the clicked element
if (e.target && e.target.matches(".list")) {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
}
//Add Items etc
});
NOTE Scots comments re eval and innerHTML apply equally to this answer.