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.
Related
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.
I want to replace a specific div element with a different one, when it has reached 3 clicks on it. That is the only task, I am trying to accomplish with the code.
I have tried looking at some code that does this but all of them replace it with get go, they don't give you a number amount to specify when to replace it with.
Example: <div id="1"></div> has been clicked on 3 times by a user. Once it exceeds that amount replace it with <div id="3"></div>
Changing the id attribute is not a good idea, instead you can use data- attribute like the following way:
var count = 0; // Declare a variable as counter
$('#1').click(function(){
count++; // Increment the couter by 1 in each click
if(count == 3) // Check the counter
$(this).data('id', '3'); // Set the data attribute
console.log($(this).data('id'));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="1" data-id="1">Click</div>
You could write a JavaScript function that keeps track how often you clicked on a specific DOM element (i. e. the div element with id="1"). As soon as the element was clicked three times, it will be replaced by another DOM element which can be created in JavaScript as well.
var clicks = 0;
function trackClick(el) {
clicks++;
if(clicks === 3) {
var newEl = document.createElement('div');
newEl.textContent = 'Div3';
newEl.id = '3';
el.parentNode.replaceChild(newEl, el);
}
}
<div id="1" onclick="trackClick(this)">Div1</div>
In case you should use a library like jQuery or have another HTML structure, please specify your question to improve this code snippet so that it fits for your purpose.
The main idea is to start listening click events on the first div and count them.
The below code shows this concept. Firstly we put first div into variable to be able to create event listeners on it and also create count variable with initial value: 0. Then pre-make the second div, which will replace the first one later.
And the last part is also obvious: put event listener on a div1 which will increment count and check if it is equal 3 each time click happens.
const div1 = document.querySelector('#id-1');
let count = 0;
// pre-made second div for future replacement
const divToReplace = document.createElement('div');
divToReplace.id = 'id-2';
divToReplace.innerText = 'div 2';
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
div1.parentNode.replaceChild(divToReplace, div1);
}
});
<div id="id-1"> div 1 </div>
Note that this approach is easy to understand, but the code itself is not the best, especially if you will need to reuse that logic. The below example is a bit more complicated - we create a function which takes 2 arguments: one for element to track and another - the element to replace with. Such approach will allow us to reuse functionality if needed.
function replaceAfter3Clicks(elem, newElem) {
let count = 0;
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
elem.parentNode.replaceChild(newElem, elem);
}
});
}
const div1 = document.querySelector('#id-1');
// pre-made second div for future replacement
const div2 = document.createElement('div');
div2.id = 'id-2';
div2.innerText = 'div 2';
replaceAfter3Clicks(div1, div2);
<div id="id-1"> div 1 </div>
If you know, how to use JQuery, just put a click event handler on your div 1. On that handler, increment a click counter to 3. If it reaches 3, replace the div with JQuery again.
If there are multiple divs to replace, use an array of counters instead of a single one, or modify a user-specific data attribute via JQuery.
Using native JavaScript, rather than relying upon library (for all the benefits that might offer), the following approach is possible:
// A named function to handle the 'click' event on the relevant elements;
// the EventObject is passed in, automatically, from EventTarget.addEventListener():
const replaceOn = (event) => {
// caching the element that was clicked (because I'm using an Arrow function
// syntax we can't use 'this' to get the clicked element):
let el = event.target,
// creating a new <div> element:
newNode = document.createElement('div'),
// retrieving the current number of clicks set on the element, after this
// number becomes zero we replace the element. Here we use parseInt() to
// convert the string representation of the number into a base-10 number:
current = parseInt(el.dataset.replaceOn, 10);
// here we update the current number with the decremented number (we use the
// '--' operator to reduce the number by one) and then we update the
// data-replace-on attribute value with the new number:
el.dataset.replaceOn = --current;
// here we discover if that number is now zero:
if (current === 0) {
// if so, we write some content to the created <div> element:
newNode.textContent = "Original element has been replaced.";
// and here we use Element.replaceWith() to replace the current
// 'el' element with the new newNode element:
el.replaceWith(newNode);
}
};
// here we use the [data-replace-on] attribute-selector to search
// through the document for all elements with that attribute, and
// use NodeList.forEach() to iterate over that NodeList:
document.querySelectorAll('[data-replace-on]').forEach(
// using an Arrow function we pass a reference to the current
// Node of the NodeList to the function, and here we use
// EventTarget.addEventListener() to bind the replaceOn function
// (note the deliberate lack of parentheses) to handle the
// 'click' event:
(element) => element.addEventListener('click', replaceOn)
);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
div {
border: 1px solid #000;
display: inline-block;
padding: 0.5em;
border-radius: 1em;
}
div[data-replace-on] {
cursor: pointer;
}
div[data-replace-on]::before {
content: attr(data-replace-on);
}
<div data-replace-on="3"></div>
<div data-replace-on="13"></div>
<div data-replace-on="1"></div>
<div data-replace-on="21"></div>
<div data-replace-on="1"></div>
<div data-replace-on="6"></div>
<div data-replace-on="4"></div>
References:
CSS:
Attribute-selectors ([attribute=attribute-value]).
JavaScript:
Arrow function syntax.
ChildNode.replaceWith().
document.querySelectorAll().
EventTarget.addEventListener().
NodeList.prototype.forEach().
I need to create an event-listener which fires when a user clicks one of the list items in the HTML.The action should call a function named listItemText which returns the innerText of the list item which was clicked--ie: if they click the first li it should log "Walk the dog"
I've tried everything I can think of to get the correct corresponding innerText of the li item that is being clicked on. At best, I've gotten either the whole list back in the console.log, or the last element of the list.
I've tried so many things at this point it would be impossible to recall. More or less the code below is a variant of what I've attempted
<ul id="todo-app">
<li class="item">Walk the dog</li>
<li class="item">Pay bills</li>
<li class="item">Make dinner</li>
<li class="item">Code for one hour</li>
</ul>
var targetUl = document.getElementById('todo-app').childNodes;
this.addEventListener('click', listItemText);
function listItemText(event) {
var liClicked = event.target;
for (i=0; i<targetUl.length; i++) {
if (liClicked == 'LI') {
console.log(targetUl[i].innerText)
}
}
}
I expect to get the text content of the li tag but I keep getting undefined at this point. Help would be greatly appreciated.
If I understand correctly you want the console.log of the text of the li element you click, so I suppose you can try this code below:
var targetUl = document.getElementById('todo-app').addEventListener("click",listItemText);
function listItemText(event) {
var liClicked = event.target;
if(liClicked && liClicked.nodeName == "LI"){
console.log(liClicked.textContent)
}
}
<ul id="todo-app">
<li class="item">Walk the dog</li>
<li class="item">Pay bills</li>
<li class="item">Make dinner</li>
<li class="item">Code for one hour</li>
</ul>
This is the principle of event delegation, where you don't need to attach event listeners on all of the elements but only to the parent node, if the event happened it will bubble up to the parent and using the event.target you can get the reference of the child element which was clicked.
I just now ended up figuring it out. Did it slightly differently, but ultimately the same idea. Was having a hard time understanding event.target and how it works. Here's my answer--which is almost the same as your answer, good sir:
var toDoList = document.getElementById("todo-app");
toDoList.addEventListener('click', listItemText);
function listItemText(e) {
console.log(e.target.innerText);
return e.target.innerText;
}
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
So I have EDIT and REMOVE buttons that are dynamically added for each data node (a "poll") in a Firebase database. I have a function which assigns onclick listeners to these with jQuery, but oddly, the event only fires when there just happens to be a single node, and hence a single pair of EDIT/REMOVE buttons. When there are multiple nodes and multiple pairs of buttons, none will fire. Here's the javascript where the events are added to the buttons...
function displayCurrentPollsForEditing(pollsRef)
{
var tbl = createTable();
var th = ('<th>Polls</th>');
$(th).attr('colspan', '3');
$(th).appendTo($(tbl).children('thead'));
pollsRef.once('value', function(pollsSnapshot) {
pollsSnapshot.forEach(function(pollsChild) {
var type = pollsChild.name();
// If this is true if means we have a poll node
if ($.trim(type) !== "NumPolls")
{
// Create variables
var pollRef = pollsRef.child(type);
var pollName = pollsChild.val().Name;
var btnEditPoll = $('<button>EDIT</button>');
var btnRemovePoll = $('<button>REMOVE</button>');
var tr = $('<tr></tr>');
var voterColumn = $('<td></td>');
var editColumn = $('<td></td>');
var rmvColumn = $('<td></td>');
// Append text and set attributes and listeners
$(voterColumn).text(pollName);
$(voterColumn).attr('width', '300px');
$(btnEditPoll).attr({
'class': 'formee-table-button',
'font-size': '1.0em'
});
$(btnRemovePoll).attr({
'class': 'formee-table-remove-button',
'font-size': '1.0em'
});
$(btnEditPoll).appendTo($(editColumn));
$(btnRemovePoll).appendTo($(rmvColumn));
// Append to row and row to table body
$(tr).append(voterColumn).append(editColumn).append(rmvColumn);
$(tr).appendTo($(tbl).children('tbody'));
// Append table to div to be displayed
$('div#divEditPoll fieldset#selectPoll div#appendPolls').empty();
$(tbl).appendTo('div#divEditPoll fieldset#selectPoll div#appendPolls');
$(btnEditPoll).click(function() {
displayPollEditOptions(pollRef);
return false;
});
$(btnRemovePoll).click(function() {
deletePoll($(this), pollsRef);
return false;
});
}
});
});
}
The markup would be something like the following...
<div id="divEditPoll">
<form class="formee" action="">
<fieldset id="selectPoll">
<legend>SELECT A POLL</legend>
<div class="formee-msg-success">
</div>
<div class="grid-12-12" id="appendPolls">
</div>
</fieldset>
</div>
EDIT - So I've switched some lines around and now I don't set the click() events until the buttons are appended to the document, so the button elements are definitely in the DOM when the click events are attached. So could this issue result from not setting id's for these buttons? That seems strange to me, since I'm using variable references rather than ids to attach the events.
There are two things I would check for.
First, make sure you don't have two elements with the same id. If you do, jquery may only bind to the first, or not bind at all.
Second, make sure the element is added to the dom before jquery attempts to bind the click event. If the code is running asynchronously, which can easily happen if you're using ajax, then you may be trying to bind the event before creating the element. Jquery would fail to find the element then give up silently.
you should use .on() for dynamically added button