Click and show? - javascript

I am working with this:
<html>
<ol onclick="myevent(event);">
<li title="a">Test 1</p>
<li title="b">Test 2</p>
</ol>
<div id="a" style="display:none;">Text to show</div>
<div id="b" style="display:none;">Other text to show</div>
<script>
function myevent(event) {
var x, i, clk, res;
x = document.getElementsByTagName("DIV");
for (i = 0; i < x.length; i++) {
x[i].style.display = "none";
}
clk = event.target.title;
res = document.getElementById(clk);
res.style.display = "block";
}
</script>
</html>
Essentially click one line item, hide all blocks, and then show the named block. This should be simple but I have been researching for hours and getting nowhere.
Thanks in advance!
- Joe

You have some typos in your code.
Firstly, you are wrongly closing li element with </p>
Secondly, getElementsBytagname should be getElementsByTagName and getelementbyid should be getElementById:
function myevent(event){
var x, i, clk, res;
x = document.getElementsByTagName("DIV");
for (i = 0; i < x.length; i++) {
x[i].style.display = "none";
}
clk = event.target.title;
res = document.getElementById(clk);
res.style.display="block";
}
<ol onclick="myevent(event);">
<li title="a">Test 1</li>
<li title="b">Test 2</li>
</ol>
<div id="a" style="display:none;">Text to show</div>
<div id="b" style="display:none;">Other text to show</div>
You can also try using querySelectorAll() and forEach() that does not require the loop variable i:
function myevent(e){
var x, clk, res;
x = document.querySelectorAll("DIV");
x.forEach(div => div.style.display = "none");
clk = e.target.title;
res = document.getElementById(clk);
res.style.display="block";
}
<ol onclick="myevent(event);">
<li title="a">Test 1</li>
<li title="b">Test 2</li>
</ol>
<div id="a" style="display:none;">Text to show</div>
<div id="b" style="display:none;">Other text to show</div>

Event Handlers
There's three ways to handle an event (in this case the click event on <ol>):
Event Attribute (not recommended)
<ol onclick="eventHandler(); return false">...</ol>
Event Property
document.querySelector('ol').onclick = eventHandler;
Event Listener (recommended)
document.querySelector('ol').addEventListener('click', eventHandler);
For details refer to Introduction to Events and DOM Onevent Handlers
Event Delegation
The following demo uses a programming paradigm called Event Delegation. The advantages over the code provided in OP (Original Post) are:
Only one tag needs to be registered as the event listener (common ancestor tag of all targeted tags - ex. <ol> is an ancestor tag to all <li>).
There's no need to loop thru each targeted tag and register each one to an event (ex. <li> do not have to be in a for loop).
There can be an unlimited number of targeted tags and knowing how many is not required. (ex. there can be a single <li> to theoretically thousands of <li>).
The targeted tags can be added dynamically anytime -- during or after page load. If done any other way, any dynamically added tags would not work without reloading the page and somehow keeping them.
Refer to Event Delegation for details.
References
In the following demo these methods and properties were used:
DOM
Document.querySelector()
Element.tagName
Node.textContent
Event
EventTarget.addEventListener()
Event.currentTarget
Event.target
Event.stopPropagation()
Demo
Details are commented in demo
/* EVENT DELEGATION */
// Register Ancestor Tag to Event
/*
Register the click event to a common ancestor tag of all
tags you want to be clickable.
So in this instance the clickable tags are all <li>
Ancestor tags could be:
window
document
<body>
<main>
<ol>
Usually the closest tag is the best choice which is <ol>
*/
// ------ Breakdown ------
/*
document.querySelector('ol')
-- find <ol>
.addEventListener('click'...
-- register click event to <ol>
..., eventHandler);
-- run function eventHandler() when <ol> is clicked
*/
document.querySelector('ol').addEventListener('click', eventHandler);
// Event Object
/*
Pass event object which will have properties and methods that will allow us to:
1. find what tag is listening for click event
- event.currentTarget
- in this demo it is <ol>
2. find what tag the user actually clicked
- event.target
- in this demo only <li> will react to being clicked
3. stop the click event from "bubbling" up the event chain
- event.stopPropagation();
- this method is called at the end of eventHandler() so
that the click event doesn't trigger anything else
*/
function eventHandler(event) {
// Reference all tags concerned
// See Event Object #1
const listener = event.currentTarget;
// See Event Object #2
const clicked = event.target;
// Find <output>
const display = document.querySelector('output');
// ------ Breakdown ------
/*
if <ol> isn't the tag that the user clicked...
... and if the clicked tag is a <li>...
... get the text inside that <li>...
... and place it in <output>
(it gets overwritten on each click so there's no need for multiple tags)
*/
if (listener !== clicked) {
if (clicked.tagName === 'LI') {
let data = clicked.textContent;
display.textContent = data;
}
}
/*
Regardless of whether any <li> were clicked stop the click event
See Event Object #3
*/
event.stopPropagation();
}
main {
font: 700 1rem/1.5 Tahoma
}
li:hover {
cursor: pointer
}
<main>
<ol>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
</ol>
Item: <output></output>
</main>

Related

Prevent click after a certain child when event handler is attached to parent

I have the following structure:
<ul id="list-items">
<li class="item" data-id="123" data-title="Some Title">
<div class="block">
<img src="#"/>
Link
<p>Some excerpt</p>
</div>
</li>
</ul>
There are more than 1 <li> item
All data- attributes are on the <li> elements
Using jQuery, I would usually make use of event delegation instead of attaching event handlers on every <li> item:
$( document ).on( 'click', '.item', function() {
var id = $( this ).data( 'id' );
var title = $( this ).data( 'title' );
});
However, I am not able to replicate this using Pure JavaScript.
I want to be able to click on an <li> item without clicking any of its child elements.
I am also not at the liberty of using closest() since we have to provide support for IE11. Is there a simpler way to implement it?
EDIT:
I am avoiding attaching event listeners to each <li> item as it won't work for dynamically created <li> elements, and also for performance reasons.
You can disable the li's content from getting any mouse event by setting a pointer-events: none to it.
<li class="item" data-id="123" data-title="Some Title">
<div class="block" style="pointer-events: none">
<img src="#"/>
Link
<p>Some excerpt</p>
</div>
</li>
You can guarantee now that the event.target will always be the li
If you don't want to attach event handler on all the items directly. you can attach only one event handler on the parent like this
var element = document.querySelector("#vanilla-parent")
element.addEventListener("click", function(event){
event.composedPath().forEach(function(elm){
if (elm.tagName === 'LI') {
// do something
}
});
});
$("#jquery-parent").on("click", "li", function(event){
// do something
});
Pen demonstrating the same: https://codepen.io/kireeti-tiki/pen/EzPpqZ?editors=1010
I used composedPath on the event object to get to li, I wouldn't recommend this as this is a bit of hacky way to get to the solution. Also, it is not supported on IE and Edge. So stay away from that solution if you need support for those browsers. More on that subject here https://developer.mozilla.org/en-US/docs/Web/API/Event
If using jQuery is not a problem, Then I would recommend that approach.
Will something like get all li and attach an eventlistener do?
<script>
var li_list = document.getElementsByTagName('li');
for(var i = 0; i < li_list.length; i++) {
(function(index) {
li_list[index].addEventListener("click", function() {
event.preventDefault();
alert('clicked')
})
})(i);
}
</script>
Here's an example using Element.closest(), for which there is a polyfill.
function attachClickHandler() {
const list = document.querySelector('.list');
list.addEventListener('click', (item) => {
// console.log('Item:', item);
const closest = item.target.closest('li');
console.log('Closest li:', closest);
console.log('Data on closest li:', closest.tagName, closest.dataset && closest.dataset.id || 'no data');
// alternative
console.log(' === alt ===');
let elem = item.target;
while (elem.tagName.toLowerCase() !== 'li') {
elem = elem.parentNode;
if (elem.tagName.toLowerCase() === 'ul') {
break;
}
}
console.log('Manual version:', elem.tagName, elem.dataset && elem.dataset.id || 'no data');
});
console.log('Listening.');
}
attachClickHandler();
<h1>My list</h1>
<ul class="list">
<li data-id="1">Item 1</li>
<li data-id="2"><button>Item 2</button></li>
<li>Empty item</li>
</ul>
Edit: This won't work on IE as stackoverflow likely doesn't load the polyfill. But if you test it standalone (should be 3 seconds work) you can verify if it's good enough for you.

Event listener on parent node is triggering on all children nodes too

I'm writing a draggable class in vanilla js from scratch (this is NOT related to jQuery draggable), and it's working great, only my listeners are triggering on children of the selected node. I've tried e.stopPropagation() inside of the listeners but it doesn't seem to change the outcome.
Here's a snippet of code from the class:
setListeners() {
const targets = document.getElementsByClassName("-jsDraggable");
[...targets].forEach(elem => {
elem.addEventListener("mousemove", e => this.handleMouseMove(e));
elem.addEventListener("mousedown", e => this.handleMouseDown(e));
elem.addEventListener("mouseup", e => this.handleMouseUp(e));
});
}
... and for example:
<ul class="-jsDraggable">
<li>No drag</li>
<li>No drag</li>
<li class="-jsDraggable">Can drag this item</li>
<li>No drag</li>
</ul>
In this scenario I would want both the <ul class="-jsDraggable">...</ul> to be affected and also the single <li class="-jsDraggable">...</li> inside, where dragging from one of the other list items would simply drag the main <ul class="-jsDraggable">...</ul> element.
However, no matter which child node I interact with, regardless of how far down in the DOM it is compared to the node with the class, it triggers the handlers.
Any help would be greatly appreciated!
EDIT: Another example to clarify:
<article class="-jsDraggable">
<header>
<h1>Heading</h1>
</header>
<main>
<p>...</p>
</main>
</article>
No matter where I drag in this <article> element, it should drag the entire group of HTML as one (since the parent of it all has the class). Currently, however, it's acting more like this:
<article class="-jsDraggable">
<header class="-jsDraggable">
<h1 class="-jsDraggable">Heading</h1>
</header>
<main class="-jsDraggable">
<p class="-jsDraggable">...</p>
</main>
</article>
... and every single child is also moving when it should only be moving the parent container that has the class attached.
I solved it, it wasn't working because I was setting the current element as this.elem = e.target instead of this.elem = e.currentTarget. After this, e.stopPropagation() in the mousedown worked as expected.
Thanks for exposing me to e.currentTarget, I wasn't aware of it before reading the help in this thread.
If I understand your question correctly, then one option (amongst others) is to distinguish the origin of the event, and filter event handling via the currentTarget and target fields of the event object like so:
if(event.currentTarget === event.target) {
/* The user is interacting with the actual -jsDraggable element */
}
else {
/* The user is interacting with a descendant of a -jsDraggable element */
}
This in effect means that, if the element that is spawning the event (ie currentTarget) is that same element that the event handler has been bound to (ie target), then we determine that the event is in relation to an actual -jsDraggable element itself (and not a descendant). This approach compliments your current code as shown:
function handleMouseMove(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Mouse move over draggable');
}
}
function handleMouseDown(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Drag start on draggable');
}
}
function handleMouseUp(event) {
if(event.currentTarget === event.target) {
console.log(Date.now(), 'Drag end on draggable');
}
}
function setListeners() {
const targets = document.querySelectorAll(".-jsDraggable");
targets.forEach(elem => {
elem.addEventListener("mousemove", e => handleMouseMove(e));
elem.addEventListener("mousedown", e => handleMouseDown(e));
elem.addEventListener("mouseup", e => handleMouseUp(e));
});
}
setListeners();
ul {
padding:1rem;
background:lightgreen;
}
li {
padding:1rem;
background:pink;
}
.-jsDraggable {
background:lightblue;
}
<ul class="-jsDraggable">
<li>No drag</li>
<li>No drag</li>
<li class="-jsDraggable">Can drag this item</li>
<li>No drag</li>
</ul>

Adding event listeners to <li> that are created using javascript

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

Pure JavaScript solution to jQuery.closest for closing a menu

I have a piece of code that closes a drop down menu, if you click somehwere on the document other than the opened menu itself. I would like to get rid of jQuery, but I'm not sure how to translate this code to pure javascript.
$(document).ready(function() {
$(document).click(function(event) {
if (!$(event.target).closest('li.main').length) {
if ($('li.main').is(":visible")) {
$('#dropdown').hide();
}
}
})
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
<li id="main" onclick="ToggleMainMenu();"><span>All categories</span>
</li>
<li> Item 1
</li>
<li> Item 2
</li>
<li> Item 3
</li>
</ul>
Your click handler is doing the following:
if ( clicked object is a descendent of "li.main" AND "li.main" is visible)
hide "#dropdown"
If you add the onclick attribute to the node "li.main" or one of its descendents - as you added ToggleMainMenu in your comment - then you guarantee:
- The clicked item is "li.main" or is a descendent of "li.main"
- "li.main" is visible (since you cannot click an invisible item)
At this point you do not need comparisons any more and you only need to hide "#dropdown" in ToggleMainMenu function's body. Copying from javascript hide/show element :
document.getElementById("dropdown").style.display = "none";
p.s. I used jquery selector notation for brevity
Sometimes it's easier than you think. I found a solution:
document.addEventListener("DOMContentLoaded", function (event) {
document.addEventListener('click', function (e) {
var mainMenu = document.getElementById('main');
if (!mainMenu.contains(e.target)) {
document.getElementById('dropdown').style.display = 'none';
}
}, false);
});
I didn't know about contains.

Event Listener in JavaScript not jQuery

$('ul.mylist').on('click', 'li', function(){})
I want to transform this jQuery line into vanilla but how I can do this ? I looked at jQuery source but I'm confused. Is there simple way to do this in pure Javascript ?
Some features here are browser dependent, but this is basically how it goes...
// select all UL elements with the `mylist` class
var uls = document.querySelectorAll('ul.mylist');
// Iterate the collection, and bind the `click` handler
for (var i = 0; i < uls.length; i++)
uls[i].addEventListener('click', handler, false);
// The `click` handler iterates starting with `event.target` through its
// ancestors until the bound element is found. Each element matching the
// "LI" nodeName will run your code.
function handler(event) {
var node = event.target;
do {
if (node.nodeName === "LI") {
// RUN YOUR CODE HERE
}
} while (node !== this && (node = node.parentNode));
}
You can make the selector test more broad by using node.matchesSelector(".my.selector"), though that method is implemented using browser-specific property flags in some browsers, so you'd need to first expose it on the Element.prototype under the proper name.
You can bind click listener to any element as follows:
<ul id="mylist">
<li> item 1</li>
<li> item 2</li>
<li> item 3</li>
<li> item 4</li>
</ul>
<script type="text/javascript">
function click_handler(){
alert('you clicked a list');
}
var list=document.getElementById("mylist");
list.onclick=click_handler;
</script>
here is JSFiddle

Categories