cannot delete last item from todo list - javascript

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/

Related

How to make the event listener only add CSS element on one item at a time (vanilla JavaScript)

^I would like to be able for the style to be enabled for only one at a time.
^I'm able to do this, which I don't want the user to be able to do.
So it's weirdly hard framing a question for what is possibly an easy solution. I basically have a list of build versions where I want the user to select one. When one of the versions are selected, it adds a border to the item to display that its clicked. However, with my code right now the user is able to select all 3 items and enable their CSS elements. I would like for the user to be able to only "activate" one item from the list.
HTML and CSS:
<ul class="listContents">
<li><p>Stable</p></li>
<li><p>Preview</p></li>
<li><p>LTS</p></li>
</ul>
<style>
.colorText {
background-color: #58a7ed;
color: white;
}
</style>
and the JS stuff:
const btn = document.querySelectorAll('.links');
for (let i = 0; i < btn.length; i++ ) {
btn[i].addEventListener("click", function() {
btn[i].classList.add('colorText')
})
}
I really hope I made myself clear, I feel like I'm failing my English trying to word this right lol.
You can also use a forEach loop, accessing the clicked link using event.target
const btns = document.querySelectorAll('.links');
btns.forEach(btn => {
btn.addEventListener('click', e => {
// remove any existing active links
btns.forEach(b => b.classList.remove('colorText'));
// activate the clicked link
e.target.classList.add('colorText');
})
});
.colorText {
background-color: #58a7ed;
color: white;
}
<ul class="listContents">
<li>
<p>Stable</p>
</li>
<li>
<p>Preview</p>
</li>
<li>
<p>LTS</p>
</li>
</ul>
Just before you add the colorText class to the desired item, we can remove colorText from ALL of them, ensuring that only 1 at a time gets the class.
// the rest is the same...
btn[i].addEventListener("click", function() {
// remove it from all:
btn.forEach(function(item) {
item.classList.remove('colorText');
});
// add it back to the desired one
btn[i].classList.add('colorText')
})
you can also use simple for of
const btn = document.querySelectorAll(".links");
for (let bt of btn) {
bt.addEventListener("click", (e) => {
btn.forEach((b) => b.classList.remove("colorText"));
e.target.classList.add("colorText");
});
}

How do I add event delegation here?

I'm making a pizza ordering application using pure javascript. In the CART section, I have two click operations - increase /decrease the item quantity and remove the item from cart. I have two event listeners for these two operations. I want to make use of event delegation here.
The DOM structure looks like this. Here the class cart-items is the parent class of all the items that are dynamically added into the card. Every item present in the cart gets a div and the ID associated with the pizza item chosen is assigned as ID for that item. For example, I add the pizza item with ID 4 in my cart, so a <div> with id 4 is added in the DOM. :-
Here are the event listeners for the increase/decrease in quantity and remove item from cart.
//event listener attached when remove button is clicked on
let removeCartItemButtons = document.getElementsByClassName("btn-danger");
for (let i = 0; i < removeCartItemButtons.length; i++) {
let button = removeCartItemButtons[i];
button.addEventListener("click", removeCartItem);
}
//event listener attatched when quantity is increased or decreased
let quantityInputs = document.getElementsByClassName("cart-quantity-input");
for (let i = 0; i < quantityInputs.length; i++) {
let input = quantityInputs[i];
input.addEventListener("change", quantityChanged);
}
MY TAKE ON:
document
.getElementsByClassName("cart-items")
.addEventListener()
);
I want to make use of event delegation and don't want to create multiple event listeners. So, I'm fetching the cart-items which is basically the parent element here for the CART section.
Now, how do I add the two event listeners inside the above code.
Please help!
for(const el of document.querySelectorAll(".cart-items")){
el.addEventListener('click', function(e){
if(e.target.matches('.btn-danger')){
removeCartItem(e.target);
}
});
el.addEventListener('change', function(e){
if(e.target.matches('.cart-quantity-input')){
quantityChanged(e.target);
}
});
}

why i cant remove last element after removing elements before it

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>

JavaScript issue on filtering text in html element

I've been struggling with the same piece of code for a few days by now...
So for the html part I have this :
<input type="text" id="search_immobilier_ville" name="search_immobilier[ville]">
<div class="collection" id="search-ville-collection"></div>
I have an input where I have to type any city name, then I want to filter the matching cities names into an existing array of cities like this :
let api_results = [['Le Moule',97152],['Lamentin',97189],...]
let ville_input = document.getElementById('search_immobilier_ville');
Then display the matching cities as a list of elements in the div #search-ville-collection.
On each keyup event, I need to perform this action, and update the visual list in real time.
My issue is that my filtering system is messed up, if I search "lam" for example, I can get a city called "lamentin" (pass the test) and another with just the "la" matching like "capesterre-de-marie-galante"
So far, I've done this :
// Previously filled by an API
let api_results = [[name,postalCode],[name,postalCode],...];
ville_input.addEventListener('keyup', () => {
let value = ville_input.value.toUpperCase().trim();
// User input
let input_val = ville_input.value.toUpperCase().trim();
// Filtering through var ville_input
api_results.forEach(el => {
if (el[0].toUpperCase().startsWith(input_val) || input_val.length >= 2 && el[0].toUpperCase().includes(input_val)) {
result_list.style.display = 'block';
// if city not present in the html list, add it
if (document.getElementById(el[1]) === null) {
$(result_list).append(`<a class="collection-item search-ville-results" id="${el[1]}"> ${el[0]} - ${el[1]} </a>`);
}
}
}); // End forEach
/* Looping through the collection child nodes to check
if there are cities that don't match the user input */
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
// Highlight first element of the list
result_list.firstElementChild.classList.add('active');
// empty results div if user input is empty
if (input_val == '') {
result_list.style.display = 'none';
result_list.innerHTML = '';
}
});
This code works PARTIALLY. For example, if I type "lam", I'm supposed to get only one result based on my result set, but check out this scenario :
Typing "l":
Typing "la":
Typing "lam":
(Here you begin to see the issue)
Typing "lame":
I'm sure there's something wrong in my code, but I can't figure out what.
Your problem is with the loop you are using to remove invalid items:
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
children returns a live HTMLCollection, meaning that if you modify it (eg, by removing items) it will update, which will cause issues with your loop. You need to go through the items in a way that will not be affected if the collection changes.
Wrong way:
This is an example of how the loop behaves currently. The button should remove all the items that contain "th", but note how it doesn't get them all and requires multiple clicks:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
for (let item of list.children) {
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
(A) correct way:
One way to loop through the collection in a way that is not affected by items being removed is to start at the last index and go backwards:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
let index = list.children.length
while (index--) {
let item = list.children[index]
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
Better way
As an additional note, you might be better off just clearing the list entirely and using filter to get the matching results and then update the list that way. As it is currently, you are doing a lot of checking to see if the list already contains the item, checking the current list for invalid items, etc. That will affect performance of your UI, especially on lower end devices.
Try to clear out your result_list as the first thing you do inside your keyup event.
result_list.innerHTML = '';
After that, make sure to filter your api_results.
const filteredResults = api_results.filter(result => result[0].toUpperCase().includes(input_val));
console.log(filteredResults); // Sanity check.
filteredResults.forEach(result => /* your old function. */);

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

Categories