Event.target.dataset returning undefined javascript - javascript

I am getting some issues while trying to get the data attribute of any html element.
The problem is i am getting the data attribute in 30% of the cases. The rest is returning undefined.
Here's what i want to trigger:
document.addEventListener("DOMContentLoaded",() => {
document.body.addEventListener("click",(e) => {
console.log("clicked");
console.log(e.target.dataset.link + " is the link clicked") // this is returning undefined most of the times.
if (e.target.dataset.link !== undefined) {
console.log("got the link")
navigateTo(e.target.dataset.link);
}
})
// router();
})
<div class="cell" data-link="/dashboard/posts" tabindex="1">
<i class="material-icons">assignment</i>
<span>Posts</span>
</div>
How is this even possible ?
And how can i prevent this ?
I can't remove the onclick event listener for the body.

event.target is the element the event was targeted at, which may be inside your data-link element (like the i and span in your div). You can use the closest method with an attribute presence selector ([attribute-name]) to find the data-link element:
const dataElement = e.target.closest("[data-link]");
That checks e.target to see if it matches the CSS selector and, if it doesn't, looks to its parent to see if it matches, and so on until it reaches the document root. If it gets all the way to the root without finding it, it returns null.
Updated Snippet:
document.addEventListener("DOMContentLoaded",() => {
document.body.addEventListener("click",(e) => {
const dataElement = e.target.closest("[data-link]");
if (!dataElement) {
return; // There wasn't one
}
console.log(dataElement.dataset.link + " is the link clicked") // this is returning undefined most of the times.
if (dataElement.dataset.link !== undefined) {
console.log("got the link")
// navigateTo(dataElement.dataset.link);
}
})
// router();
})
<div class="cell" data-link="/dashboard/posts" tabindex="1">
<i class="material-icons">assignment</i>
<span>Posts</span>
</div>
However, please note evolutionxbox's comment. You're recreating basic web functionality using non-semantic elements and JavaScript code. That destroys the accessibility of your page, and even for users who don't rely on assistive technology, it makes it impossible for them to use the advanced features of their browser to (say) open the link in a new tab, etc.

You attach the event listener to the document body.
Is absolutely normal that you don't get the dataset: you can click out of the cell, or in a element into this.
You need attach the event to the desired elements with the data-link attribute:
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-link]').forEach(node => {
node.addEventListener("click", (e) => {
console.log("clicked");
console.log(node.dataset.link + " is the link clicked")
})
})
})
<div class="cell" data-link="/dashboard/posts">
<i class="material-icons">assignment</i>
<span>Posts</span>
</div>
<div class="cell">
<i class="material-icons">assignment</i>
<span>Posts</span>
</div>

Related

Open hidden elements one at a time?

please I am trying to create a FAQ like functionality, I have some elements hidden so when I click on a button it opens and hides it. I have been able to do this but I am not getting what I actually want. I might have done something wrong I suppose. So, there are 5 elements with the same className, this will help me target them all and run a for loop to kind of break them apart. However if I click on this button to open one of the element the other ones open.
const openBtn = document.querySelectorAll(".openBtn")
const openContent = document.querySelectorAll(".openContent")
for(btn of openBtn) {
btn.addEventListener('click', () => {
for(content of openContent) {
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
content.classList.add('flex')
} else {
content.classList.remove('flex');
content.classList.add('hidden')
}
}
})
}
So as you can see, If I click on the chevron icon for just one of the wither About Us, Careers or just any of the 5 every other one opens. How do I fix this ?
Since you aren't going to post even the most general version of your HTML, here is a general outline.
First, each button gets a data attribute for target,then each FAQ div gets an ID attribute that matches the data target attribute.
I attach the click handler to the document and look for openBTN on the clicked element. Then I loop through every OPENED div to close it. Then I get the target data attribute and add the appropriate classes.
document.addEventListener("click", function(e) {
if (e.target.classList.toString().includes("openBtn")) {
let opened = document.querySelectorAll(".openContent.flex");
opened.forEach(function(el) {
el.classList.add("hidden");
el.classList.remove("flex");
});
let target = document.querySelector(e.target.dataset.target)
target.classList.remove("hidden");
target.classList.add("flex");
}
});
.hidden {
display: none
}
<button data-target="#faq1" class="openBtn">OPEN</button>
<div id="faq1" class="openContent hidden">1</div>
<button data-target="#faq2" class="openBtn">OPEN</button>
<div id="faq2" class="openContent hidden">2</div>
<button data-target="#faq3" class="openBtn">OPEN</button>
<div id="faq3" class="openContent hidden">3</div>
<button data-target="#faq4" class="openBtn">OPEN</button>
<div id="faq4" class="openContent hidden">4</div>

The value of my newly appended button returns undefined or nothing at all

I am trying to obtain the value of a newly appended button, however it always logs undefined or nothing. I have tried many methods, such as .val(), .textContent, and .value, as I have found those on here.
Here is my code.
}).done(function (response) {
var lat = response.data[0].latitude;
var long = response.data[0].longitude;
//Appends new button based on recent search
searchHistory.append(`<button class="col-12 btn border-info m-1" id="prevSearch">${textInput.val().toLowerCase().split(' ').map((s) => s.charAt(0).toUpperCase() + s.substring(1)).join(' ')}</button>`);
var previousSearch = $("#prevSearch");
previousSearch.on('click', () => {
console.log($(this).val();
console.log(document.getElementById("prevSearch").textContent);
})
})
The first log under the click function returns undefined, while the second one returns the actual content. However it only works with the first button that is appended when I try a console.log("test").
So in summary, I have 2 Issues, I can't get the value of the button and only the first button works when tested with a simple console log.
Any help would be greatly appreciated.
You're getting the button by ID. If you're adding multiple elements with the same ID, that won't work since IDs must be unique within the document.
As for your button value, I'd say just use $(this).text(). Browser support for the element content as HTMLButtonElement.value has a patchy history.
I'd just use the following
const button = $("<button>", {
type: "button",
"class": "prevSearch col-12 btn border-info m-1",
text: textInput.val().replace(/(^| )[a-z]/g, c => c.toUpperCase())
}).appendTo(searchHistory)
button.on("click", function() {
console.log($(this).text())
})
You could also move the event handling to a delegated handler outside of this code that can handle all current and future button clicks
searchHistory.on("click", ".prevSearch", function() {
console.log($(this).text())
})

Get element data-attributes and insert them into the button

I want to ensure that when I click on the divs (A, B, C), the link of the button changes and gets the values of the data attributes in the appropriate places. I wrote a small script, but it does not work, and there is still not enough knowledge to understand exactly where I went wrong. Any help would be welcome.
document.getElementById("product").onclick = function() {
document.getElementById("purchase").href =
"/?add-to-cart=" + this.data-product +
"&variation_id=" + this.data-id + "/";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="product__items" id="product">
<div data-id="338" data-product="A" id="uI-1" class="items-uniqueItem">A</div>
<div data-id="339" data-product="B" id="uI-2" class="items-uniqueItem">B</div>
<div data-id="340" data-product="C" id="uI-3" class="items-uniqueItem">C</div>
<div class="product__items---btn">
Button
</div><!-- btn -->
</div>
You have several problems here.
First, I suggest you consult the documentation for HTMLElement.dataset or jQuery's .data().
Also, if you intend on using event delegation, you can't use this to refer to the event source element in a vanilla event listener as it will refer to the delegate.
Since you do have jQuery involved, you might as well use it since it makes this a lot easier (see also vanilla JS version below)
const button = $("#purchase")
$("#product").on("click", ".items-uniqueItem[data-id][data-product]", function() {
// Due to the selector above, `this` is now the clicked `<div>`
// Extract data properties
const { product, id } = $(this).data()
// Construct URL parameters
const params = new URLSearchParams({
"add-to-cart": product,
"variation_id": id
})
// Set the `href`
button.prop("href", `/?${params}/`)
})
/* this is just for visibility */
.items-uniqueItem{cursor:pointer;}#purchase{display:block;text-decoration:none;margin: 1rem;}#purchase:after{content:attr(href);display:block;color:#ccc;margin:.5rem;}
<!-- your HTML, just minified -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><div class="product__items" id="product"><div data-id="338" data-product="A" id="uI-1" class="items-uniqueItem">A</div><div data-id="339" data-product="B" id="uI-2" class="items-uniqueItem">B</div><div data-id="340" data-product="C" id="uI-3" class="items-uniqueItem">C</div><div class="product__items---btn">Button</div></div>
A vanilla JS version would look something more like this. You can use Element.closest() to locate the delegated event source
const button = document.getElementById("purchase")
document.getElementById("product").addEventListener("click", e => {
// find the required event source element
const el = e.target.closest(".items-uniqueItem[data-id][data-product]")
if (el) {
// Extract data properties
const { product, id } = el.dataset
// Construct URL parameters
const params = new URLSearchParams({
"add-to-cart": product,
"variation_id": id
})
// Set the `href`
button.href = `/?${params}/`
}
})
.items-uniqueItem{cursor:pointer;}#purchase{display:block;text-decoration:none;margin: 1rem;}#purchase:after{content:attr(href);display:block;color:#ccc;margin:.5rem;}
<!-- your HTML, just minified -->
<div class="product__items" id="product"><div data-id="338" data-product="A" id="uI-1" class="items-uniqueItem">A</div><div data-id="339" data-product="B" id="uI-2" class="items-uniqueItem">B</div><div data-id="340" data-product="C" id="uI-3" class="items-uniqueItem">C</div><div class="product__items---btn">Button</div></div>
As you can see, it's not very different to the jQuery version so maybe you might not need jQuery
I've never personally used the element.onlick = function() {...} notation, so I'll be usingelement.addEventListener('click', (e) => ...), but it should work the same way.
What you are doing is selecting the object that has the id "product". But "product" is the parent os the elements you want to select.
If you want to select several elements and do something with them, you can't use the id attribute, since id is unique for html page. So you'll want to use classes for that.
Create a class and add that class to each child (the ones with the data-product).
Select all children with .querySelectorAll(). Here is the doc. This returns a NodeList, but it's similar to an Array.
Iterate thought the List with a .forEach(item => ...) where item represents each element of the list.
Add an Event Listener (or .click, I guess) on each item.
*theList*.forEach( (item) => {
item.addEventListener('click', (event) => {
event.target.href = "/?add-to-cart=" + event.target.dataset.product + "&" + "variation_id=" + event.target.dataset.id + "/";
})
));
To access a dataset in JS you use the .dataset property.
First, grab all the divs that have a given class so that we can use their data.
const items = document.querySelectorAll('.items-uniqueItem');
items.forEach(item => item.addEventListener('click', (e) => console.log(e.target)))
Then inside you click handler you can get the button reference and assign the properties you want to get from it.

JS how to simplify

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.

How to add multiple onclick events to all elements in page

I've a HTML code like this.
<a onclick="prompt('Complete','Lorem');">Lorem</a>
<a onclick="prompt('Complete','ipsum');">ipsum</a>
<a onclick="prompt('Complete','dolor');">dolor</a>
<a onclick="prompt('Complete','sit');">sit</a>
<a onclick="prompt('Complete','amet');">amet</a>
...
I want to minify HTML code, like this: <a>Lorem</a><a>ipsum</a>How can I add onclick prompt event to all clickable elements in a page? as in the above code. Is it possible?
Using JavaScript, you have to attach the click handler to each item with a loop.
function userPrompt(event){
prompt("Complete " + event.target.innerText);
}
document.querySelectorAll('a').forEach(item => item.addEventListener('click', userPrompt));
a {
cursor: pointer
}
<a>Lorem</a>
<a>ipsum</a>
<a>dolor</a>
<a>sit</a>
<a>amet</a>
JQuery has a simple way of achieving this.
function userPrompt(event){
prompt("Complete " + event.target.innerText);
}
$('a').on('click', userPrompt);
a {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<a>Lorem</a>
<a>ipsum</a>
<a>dolor</a>
<a>sit</a>
<a>amet</a>
Like pointed out, addEventListener is your friend here.
One major advantange of addEventListener compared to say a normal onClick, is that any elements added to the DOM later will also be taken into account, and is also possible to add multiple event listeners to the same element.
Below is a simple example. I basically add the eventListener to the body, filter out any elements that are not A links, and then show a prompt for the user to change the innerText of this element.
document.body.addEventListener("click", (e) => {
//lets limit to just A links
if (e.target.tagName !== "A") return;
const ret = prompt("Confirm", e.target.innerText);
if (ret !== null) {
e.target.innerText = ret;
}
});
a {
text-decoration: underline;
cursor: pointer;
}
<div>Click a link to change the innerText</div>
<a>Lorem</a>
<a>ipsum</a>
<a>dolor</a>
<a>sit</a>
<a>amet</a>
// select all <a> tags
document.querySelectorAll('a')
// loop over them
.forEach(a =>
// append the event by calling addEventListener
a.addEventListener('click', () => window.prompt('Complete', 'Lorem')))
The forEach can take a second argument, the index, so you can define the message on each prompt according to the value of an array.
const promptValue = [
'Lorem',
'ipsum',
'dolor',
'sit',
'amet'
]
document.querySelectorAll('a').forEach((a, i) =>
a.addEventListener('click', () => window.prompt('Complete', promptValue[i])))
Edit: I should probably add that this may become hard to maintain if the list changes order in the future, so it's probably better to keep some reference to the prompt value in the HTML, even if it gets verbose. Nevertheless, it's bad to keep scripts in the HTML, so a data attribute might be a better approach.
HTML:
<a data-prompt="Lorem">Lorem</a>
<a data-prompt="ipsum">ipsum</a>
<a data-prompt="dolor">dolor</a>
<a data-prompt="sit">sit</a>
<a data-prompt="amet">amet</a>
JS:
document.querySelectorAll('a').forEach(a =>
a.addEventListener('click', () => window.prompt('Complete', a.dataset.prompt)))

Categories