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)))
Related
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>
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.
I'm supposed to clone some elements with Jquery and it works well but when i delete the first element which i'm cloning the others with it, after that the new cloned elements don't have the events which supposed to have!
i tried .clone(true, true) method and it clone event but not after the deleting the first element.
var card = $(".newCard"); //class name of first element
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true)); //it works well but...
});
$("[class^=btnDelete]").click(function() {
$(this).closest(".newCard").remove(); //it works too but not after deleting first element and creating again
});
I don't know why this is happening, actually every element should have the events even after deleting the first element and recreate.
The problem is the click event is being bound to that first element, and as a result that binding is removed along with the element. When dealing with dynamic elements you should use event delegation by using the .on method on a static element. Such as the document itself.
EDIT: You won't notice any performance issues on a small document like this, but using the document as your event delegator can cause performance issues on larger documents. You can read more about event delegation performance here.
var card = $(".newCard");
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true));
});
$(document).on('click', '.btnDelete', function() {
$(this).closest(".newCard").remove();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="addBtn0">Add</button>
<div class="row">
<div class="newCard">Card <button class="btnDelete">Delete</button></div>
</div>
This happens because of
$("[class^=btnDelete]").click(function() {
the above line will target the existing (!!!) element and it's inner button.
Since you're cloning that existing element, you're also cloning the Event bound to it's button on-creation.
Once you delete that card (stored in variable), you're also destroying the Event bound to it.
To fix that issue use .on() with dynamic event delegation:
$(".row").on('click', '[class^=btnDelete]', function() {
var card = $(".newCard"); //class name of first element
$("#addBtn0").click(function() {
$(".row").append(card.clone(true, true));
});
$(".row").on('click', '[class^=btnDelete]', function() {
$(this).closest(".newCard").remove();
});
<div class="row">
<div class="newCard">CARD <button class="btnDelete">DELETE</button></div>
</div>
<button id="addBtn0">ADD</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
https://api.jquery.com/on/#direct-and-delegated-events
Other issues and solution
Other issues you have in your code are mainly naming stuff. [class^=btnDelete] is just waiting for you one day adding a style class to that poor button and see your JavaScript fail miserably. Also, why btnAdd0 what's the 0? Why .clone(true, true) at all?
Here's a better rewrite:
const $Cards = $('.Cards');
const $Cards_add = $('.Cards-btn--add');
const $Cards_item = (html) => {
const $self = $('<div/>', {
appendTo: $Cards,
class: `Cards-item`,
html: html || `New Card`,
});
$('<button/>', {
appendTo: $self,
class: `Cards-btn Cards-btn--delete`,
text: `Delete`,
on: {
click() {
$self.remove();
}
},
});
}
let card_id = 0;
$Cards_add.on('click', () => $Cards_item(`This is Card ${++card_id}`));
// Create some dummy cards
$Cards_item(`This is Card ${++card_id}`);
$Cards_item(`This is Card ${++card_id}`);
$Cards_item(`This is Card ${++card_id}`);
/**
* Cards component styles
*/
.Cards {} /* scss extend .row or rather define styles directly */
.Cards-item { padding: 5px; margin: 5px 0; background: #eee; }
.Cards-btn { }
.Cards-btn--add { color: blue; }
.Cards-btn--delete { color: red; }
<div class="Cards"></div>
<button class="Cards-btn Cards-btn--add">ADD</button>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>
$(".row").append(card.clone(true, true));
You are still using the original result of $('.newCard') that does not include any new cards you've added.
$(".row").append($(this).parent().clone(true, true));
this works
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
I am trying to write a tutorial for my students, in the form of a webpage with hidden "spoilers" that the student can unhide, presumably after thinking about the answer. So, long story short, the behavior I am looking for is:
in the beginning, the text appears with a lot of hidden words;
when a piece of text is clicked, it appears, and stays uncovered afterwards;
this should work with minimal overhead (not forcing me to install a complex framework) and on all my students' machines, even if the browser is outdated, even if jquery is not installed.
I searched for off the shelf solutions, but all those I checked were either too complicated or not doing exactly what I wanted. So I decided to do my own.
What I have so far is this:
<HTML>
<STYLE>
span.spoil {background-color: black;}
span.spoiled {background-color: white;}
</STYLE>
<HEAD>
<TITLE>SPOIL</TITLE>
<META http-equiv="Content-Type" content="text/html;charset=UTF-8">
<!--LINK rel="Stylesheet" type="text/css" href=".css"-->
</HEAD>
<BODY>
This is a text with <span class="spoil" onclick="showspoil(this)">spoil data</span>.
<br>
<span class="spoil" onclick="showspoil(this)">Unspoil me.</span>
<br>
<span class="spoil" onclick="showspoil(this)">And me.</span>
<script>
function showspoil(e) {
e.className="spoiled";
}
// var classname = document.getElementsByClassName("spoil");
// for (var i = 0; i < classname.length; i++) {
// classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
// }
</script>
</BODY>
</HTML>
It does the job, except that I find it annoying to have to write explicitly the "onclick..." for each element. So I tried adding an event listener to each member of the class, by imitating similar resources found on the web: unfortunately, this part (the commented code above) does not work. In particular, I do not see which parameter I should pass to the function to transmit "the element itself".
Can anyone help? If I may play it lazy, I am more looking for an answer to this specific query than for pointers to a series of courses I should take: I admit it, I have not been doing html for a loooooong time, and I am sure I would need a lot of readings to be efficient again: simply, I do not have the time for the moment, and I do not really need it: I just need to solve this issue to set up a working solution.
Problem here is you are calling the method and assigning what it returns to be bound as the event listener
classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
You can either use a closure or call the element directly.
classname[i].addEventListener('click', function () { showspoil(this); }, false);
or
classname[i].addEventListener('click', showspoil, false);
If you call it directly, you would need to change the function to
function showspoil(e) {
this.className="spoiled";
}
Another option would be to not bind click on every element, just use event delegation.
function showspoil(e) {
e.className="spoiled";
}
document.addEventListener("click", function (e) { //list for clcik on body
var clicked = e.target; //get what was clicked on
if (e.target.classList.contains("spoil")) { //see if it is an element with the class
e.target.classList.add("spoiled"); //if it is, add new class
}
});
.spoil { color: red }
.spoiled { color: green }
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
function unspoil() {
this.className = "spoiled"; // "this" is the clicked object
}
window.onload = function() {
var spoilers = document.querySelectorAll(".spoil"); // get all with class spoil
for (var i = 0; i < spoilers.length; i++) {
spoilers[i].onclick = unspoil;
}
}
span.spoil {
background-color: black;
}
span.spoiled {
background-color: white;
}
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
An additional approach could be to add the click-listener to the document and evaluate the event target:
document.addEventListener("click", function(e){
if (e.target.className == "spoil"){
e.target.className = "spoiled";
}
});
That way
you only need one event listener in the whole page
you can also append other elements dynamically with that class without the need for a new event handler
This should work, because the event's target is always the actual element being clicked. If you have sub-elements in your "spoil" items, you may need to traverse up the parent chain. But anyway I think this is the least resource-wasting way.
var spoilers = document.getElementsByClassName('spoil');
for(i=0;i<spoilers.length;i++){
spoilers[i].addEventListener('click',function(){
this.className = "spoiled";
});
}