There are 6 li elements each with a remove button as child Node removing the li when clicked
But when i delete a li element after deleting its sibling (before it in position and deletion) then console show following error :"TypeError: cannot read property of undefined 'parentNode' of undefined" .
I have started learning js but i dont understand this error
as normally the position also should change as it happens in array or vectors when we delete a middle element
Example. if i delete 3rd element and then try to delete 4th element(that was just after the 3rd element) then this error ocurs and the li is also not deleted
let removeli=document.getElementsByClassName('remove');
for(let i=0;i<upperli.length;i++){
removeli[i].addEventListener('click',()=>{
//parent node pn i.e. li
let pn = removeli[i].parentNode;
//parent node of pn i.e. ul thats why ulpn
let ulpn = pn.parentNode;
ulpn.removeChild(pn);
})
}
The problem is that your event listener is forming a closure position in the list that gets passed directly into the event handler function. In other words, when you call ulpn.removeChild(pn), the value of i is not changing. If you were to re-query the DOM, you'd find the positions have shifted, but you are storing your own positions in removeli and upperli as i.
Try something like this:
let removeLi = document.getElementsByClassName('remove');
for (let i = 0; i < removeLi.length; i++) {
removeLi[i].addEventListener('click', ((pn) => {
//parent node of pn i.e. ul thats why ulpn
let ulpn = pn.parentNode;
ulpn.removeChild(pn);
})(removeLi[i].parentNode));
}
The key is that the event listener function no longer depends on i.
You can use event delegation instead of working with live HTMLCollection, so you can listen clicks also for elements added dinamically; test the "Add item" button.
// Listen clicks on the parent
let ul = document.querySelector('ul');
ul.addEventListener('click', e => {
// Was a button with "remove" class clicked?
if(e.target.classList.contains('remove')) {
// Get parent LI and remove it directly
e.target.closest('li').remove();
}
// If you have edit buttons, you can manage those clicks here
if(e.target.classList.contains('edit')) {
// Do your stuff here
}
});
// Add new element
document.querySelector('#add').addEventListener('click', () => {
// This is just an example
ul.innerHTML += '<li>New element <button class="remove">Remove</button></li>';
});
<ul>
<li>Item 1 <button class="remove">Remove</button></li>
<li>Item 2 <button class="remove">Remove</button></li>
<li>Item 3 <button class="remove">Remove</button></li>
<li>Item 4 <button class="remove">Remove</button></li>
<li>Item 5 <button class="remove">Remove</button></li>
</ul>
<button id="add">Add item</button>
Related
I have a parent container with a number of elements, it's not a well defined number of elements, it's generated by using an API once the user starts typing something in an input field. Also these elements are parents to other elements. (I'll give an example below)
The idea is that once the user clicks that element, I want to display an overlay with more information about that specific element. Sounds good, but the things I've tried didn't work so far.
I tried to add an event listener onto the container. Consider this basic HTML template.
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span>X</span></li>
<li><span>Item 4</span><span>X</span></li>
</ul>
<form>
<input type="text" id="test">
</form>
So here we have an UL which is the parent element and LI are its children. However, LI are also parents to the span tags that are supposedly showing some vital information so I cannot remove those tags.
The input field is here just to add something dyamically, to mimik the way my API works.
So like I said, I have tried to add an event listener on the UL parent >>
const items = ul.querySelectorAll('li');
items.forEach(item => item.addEventListener('click', function(e) {
console.log(this);
}));
});
});
Then I tried to add an event listener to each of the items, but instead of logging one item at a time as I click them, it's logging one, two, three and so forth.
I feel that I'm missing something here and I don't know what it is. Can you guys help me out?
Later edit: I found this solution but it doesn't seem very elegant to me and it's prone to bugs if I change stuff later on (like add more children etc).
ul.addEventListener('click', e => {
const items = ul.querySelectorAll('li');
if(e.target.tagName === 'LI' || e.target.parentElement.tagName === 'LI') {
if(e.target.tagName === 'LI') {
e.target.remove();
} else if(e.target.parentElement.tagName === 'LI') {
e.target.parentElement.remove();
}
}
});
You can use the function Element.closest() to find a parent element.
const ul = document.querySelector('ul');
ul.addEventListener('click', e => {
let li = e.target.closest('LI');
li.remove();
});
document.forms.newitem.addEventListener('submit', e => {
e.preventDefault();
let newitem = document.createElement('LI');
newitem.innerText = e.target.test.value;
ul.appendChild(newitem);
});
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span><em>X</em></span></li>
<li>Item 4 X</li>
</ul>
<form name="newitem">
<input type="text" name="test">
</form>
The reason that you got all line events every time that you've clicked in a line, is that you are add a event click in ul element and you just need it in the li element
A way very similar that you'd like:
let ul = document.querySelector("ul");
const items = ul.querySelectorAll("li");
items.forEach((item) =>
item.addEventListener("click", function (e) {
let getTargetRow = e.target.parentElement;
if (getTargetRow.nodeName === "LI"){
console.log("the line removed:",getTargetRow.textContent);
getTargetRow.remove();
}
})
);
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span>X</span></li>
<li><span>Item 4</span><span>X</span></li>
</ul>
<form>
<input type="text" id="test" />
</form>
I'm making a to-do list application. I want to delete items by clicking a button attached to the list element, but it only deletes the button and not the entire element. Currently, <li> elements in a <ul> by the following:
function newElement() {
event.preventDefault(); // stop default redirect
var li = document.createElement("li"); // create an <li> element
/* make a text node from the input and put it in the <li> */
var inputValue = document.getElementById("task").value; // retrieve value of text box
var t = document.createTextNode(inputValue); // create a text node of the box value
li.appendChild(t); // put the text node in the single <li>
/* attach a button to the <li> element that deletes it */
var btn = document.createElement("BUTTON"); // Create a <button> element
btn.innerHTML = "X"; // Insert text
btn.className = "button" // add class name for CSS targeting
btn.addEventListener('click', removeItem); // add event listener for item deletion
li.appendChild(btn); // Append <button> to <li>
li.addEventListener('click', checkToggle); // add event listener for a click to toggle a strikethrough
appendItem("list", li); //append the li item to the ul
}
and the function called by the button's listener appears as:
function removeItem() {
this.parentNode.removeChild(this);
}
I want the button to delete the entire <li> node, but it only deletes the button portion of it.
Just looking at this, I think you need to add another parentNode. It seems you are only removing the button right now.
Just move this one step further up the hierarchy
function removeItem() {
this.parentNode.parentNode.removeChild(this.parentNode);
}
Use remove() so you are not dealing withe child element references
function removeItem() {
this.parentNode.remove()
// this.closest('li').remove()
}
or since you have a reference, just delete it
btn.addEventListener('click', function () { li.remove() })
const list = document.getElementById('list')
const addtodo = document.getElementById('addtodo')
//const items = document.querySelector('.item')
const deleteItem = document.querySelector('.delete')
addtodo.addEventListener('keypress', submitTodo)
function submitTodo(event) {
if (event.keyCode == 13) {
event.preventDefault()
let value = addtodo.value
let li = document.createElement('li')
li.innerHTML = `
<img class="unchecked" src="icon/unchecked.svg" />
${value}
<img class="delete" src="icon/icons8-multiply-26.png" /> `
list.appendChild(li)
}
}
deleteItem.addEventListener('click', items)
function items(item) {
if (item.target.classList.contains('delete')) {
item.target.parentElement.remove()
}
}
The code above only allows me to delete one item and its the first one on the list
I try to solve it on my own but couldn't any idea whats wrong
When deleteItem.addEventListener('click', items) is ran, it only attaches the eventListener to the elements currently on the DOM - the ones you create dynamically will not have this eventListener
You can use 'event delegation' instead to listen for clicks, and filter theses clicks based on if the click was coming from the correct element
You can read more about event delegation from davidwalsh's blog and this StackOverflow answer
document.addEventListener('click', function(e) => {
if(e.target && e.target.classList.includes('delete')){
e.target.parentElement.remove()
}
});
You could also make use of the elements onclick attribute, and pass this in the parameter of the function call - this way you can access the HTML Element directly from the parameter; this also avoids having to have an eventListener, or using an if to check if it's the correct class / ID
// Add the function to the onclick attribute of the img
<img class="delete" onclick="deleteItem(this)" src="demo.png" />
// in this function, 'item' refers to the DOM Element that was clicked
function deleteItem (item) {
item.parentElement.remove();
}
This code will allow you to remove the first element on ul .
let list = document.getElementById("list"); // ul element
let remove = document.getElementById("remove"); // remove button
// on double click event
remove.onclick = () => {
// remove the first child from the list
list.removeChild(list.firstChild);
}
<ul id="list">
<li>One</li>
<li>Two</li>
<li>three</li>
</ul>
<input type="button" id="remove" value="Remove First"></input>
I've made a simple todo list which so far has 2 functions. Checks off an item and deletes an item from a list.
I can delete all items if I start deleted from the bottom of the list but when I delete from the top, the first item gets deleted, the second item (now the first), deletes the item after it and then doesn't delete itself when I click on the X
I thought a stopPropagration() would help but doesn't seem to. It only stops the other checked function from running.
<ul>
<li><span>X</span> Code something</li>
<li><span>X</span> Wake up early</li>
<li><span>X</span> Buy popcorn</li>
</ul>
/**
* This strikes through list item on click once and item is marked as done
*/
const listItem = document.getElementsByTagName('li')
for (let i = 0; i < listItem.length; i++) {
listItem[i].addEventListener('click', () => {
listItem[i].classList.toggle('checked')
})
}
/**
* This deletes an item from the list on click
*/
const deleteItem = document.getElementsByTagName('span')
for (let i = 0; i < deleteItem.length; i++) {
deleteItem[i].addEventListener('click', e => {
deleteItem[i].parentNode.remove()
e.stopPropagation()
})
}
https://jsfiddle.net/k2u8mqes/
Expected result is that I should be able to delete each item, in any order
Your for loop is assigning some clicks to different elements from what you intend.
This suggestion uses the target of the click event to decide what needs to be deleted in real time. (Specifically, it removes the li that is the parent of the clicked span.)
document.addEventListener("click", checkLi);
document.addEventListener("click", deleteSpanParent);
function checkLi(event){
if(event.target.tagName == "LI"){
event.target.classList.toggle("checked");
event.target.style.color = "grey";
}
}
function deleteSpanParent(event){
if(event.target.tagName == "SPAN"){
let span = event.target, li = span.parentNode, ul = li.parentNode;
ul.removeChild(li);
}
}
<ul>
<li><span>X</span> Code something</li>
<li><span>X</span> Wake up early</li>
<li><span>X</span> Buy popcorn</li>
</ul>
You are modifying an array while iterating it, which is a common caveat.
When you delete from top:
deletes the 1st item, calls deleteItem[0].parentNode.remove(), removes the 1st element of deleteItem array, it's okay;
deletes the 2nd item, calls deleteItem[1].parentNode.remove(), but the deleteItem is now of size 2, you are actually deleting the
3rd element of the original array;
deletes the 3rd item, calls deleteItem[2].parentNode.remove(), but the deleteItem is now of size 1, you are running out of index;
Working snippet:
for (let i = 0; i < deleteItem.length; i++) {
deleteItem[i].addEventListener('click', e => {
e.target.parentNode.remove()
})
}
The problem is where the way you have used for loops to bind event listeners and how you're using accessing the particular element which the event bound to.
Instead of doing this;
listItem[i].classList.toggle('checked')
//and
deleteItem[i].parentNode.remove()
You can use the event listener's event object to access to bound element, like this;
e.target.classList.toggle('checked')
//and
e.target.parentNode.remove()
Please refer to the event listener API here: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
check this fiddle for a working example of your code : https://jsfiddle.net/491rLbxj/
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