I am having an issue with my javascript function in regards to removing elements. What I have below is two lists inside a div menu with each <li> marked by a class according to the category of drink and the drinks themselves. I also have a button that opens a modal box where the user would type in a drink category to remove. I decided to approach this by naming <li> with classes by the drink category and then taking a js function to get the elements by class name from the input text node and to remove those elements with what the user typed.
<div class = "menu">
<ul>
<li class = "coffee">french press</li>
<li class = "tea"><a href="#">english breakfast/a></li>
<li class = "milk">whole</li>
</ul>
<ul>
<li class = "coffee">dark roast</li>
<li class = "tea">green tea</li>
<li class = "milk">two percent</li>
</ul>
</div>
<button type="button" id ="openmodal">Click Me!</button>
<div id="myDeleteModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<h1 class="modal-title">
<b>Type the drink category you want to remove </b>
</h1>
<input id="deletedrinktext" type="text" />
<button id="delete">Remove</button>
<button type="button" class="btn btn-defaultdeletedrink" id="closedbtn" data-dismiss="modal" onclick="">Close</button>
</div>
</div>
<script>
var modal = document.getElementById("myDeleteModal");
// Get the button that opens the modal
var btn = document.getElementById("openmodal");
var closebtn = document.getElementById("closedbtn");
// When the user clicks the button, open the modal
btn.onclick = function () {
modal.style.display = "block";
}
closebtn.onclick = function (event) {
modal.style.display = "none";
}
</script>
<script>
(function () {
document.querySelector('#delete').addEventListener('click', function () {
let inputRemover = document.querySelector('#deletedrinktext');
let itemRemover = document.createTextNode(inputRemover.value);
let listRemover = document.getElementsByClassName(itemRemover);
for (var i = 0; i < listRemover.length; i++) {
if (document.contains(listRemover)) {
listRemover[i].remove();
}
}
inputRemover.value = ""; // clear input
});
})();
</script>
So what I want to replicate is a user would open the modal box, type in coffee and click remove. This would remove from the document the following two elements:
<li class = "coffee">french press</li>
<li class = "coffee">dark roast</li>
This function isn't working so far and I am not sure if there is an error in my JS in getting each element or if going with the class approach is not the way to go about it? When I type in the name of the category just like written in my HTML, the element in the list still displays.
remove() is not a function of Array in javascript. I think your best approach would be to just not display the elements you want to remove. To do just change your handler function to this:
let inputRemover = document.querySelector('#deletedrinktext');
let listRemover = document.getElementsByClassName(inputRemover.value);
for (let i = 0; i < listRemover.length; i++) {
if (document.contains(listRemover[i])) {
listRemover[i].style.display = 'none';
}
}
JSFiddle to try it out without modal functionality.
This also helps you if you maybe want to reset the list. To do so, just set the display property on every element to block.
Also, I don't knwo if it is a copy paste issue but the a at english breakfast is missing a < in the closing tag.
Related
I'm making a task board project.
Must say I'm using only HTML, CSS, JS, and nothing else right now.
I'm making a fade-in effect to the newly added note (ul element), and I would like to delete the fade-in class from the previously added note.
this is a chunk of my code that displays the note inside the div.
function displayAllTasks(allTasks){
taskNotesDiv.innerHTML = "";
for(const task of allTasks){
const index = allTasks.indexOf(task);
const note = `
<div class"noteDiv">
<ul class="fadeInNote">
<button type="button" onclick="deleteTask(${index})">
<i class="fa-solid fa-trash deleteButton"></i>
</button>
<li>Task: ${task.task}</li>
<li>Description: ${task.textArea}</li>
<li>Date: ${task.date}</li>
<li>Time: ${task.time}</li>
</ul>
</div>
`
taskNotesDiv.innerHTML += note;
}
}
I tried already another function to delete it but with no success.
any help would be appreciated!
There can be multiple approaches, but my approach is to create element using document.createElement . The modified JS will become:
function displayAllTasks(allTasks) {
last_ul = null; // store the last ul element added
taskNotesDiv.innerHTML = "";
for (const task of allTasks) {
const index = allTasks.indexOf(task);
let noteDiv = document.createElement('div');
noteDiv.classList.add('noteDiv');
note_ul = document.createElement('ul');
note_ul.classList.add('fadeInNote');
note_ul.innerHTML = `
<button type="button" onclick="deleteTask(${index})">
<i class="fa-solid fa-trash deleteButton"></i>
</button>
<li>Task: ${task.task}</li>
<li>Description: ${task.textArea}</li>
<li>Date: ${task.date}</li>
<li>Time: ${task.time}</li>`
noteDiv.appendChild(note_ul);
// if it is not the first element, then remove the class from previous
if (last_ul != null) {
last_ul.classList.remove('fadeInNote');
}
last_ul = note_ul; // this becomes previous for next iteration
taskNotesDiv.appendChild(noteDiv);
}
// remove class of the last element
if (last_ul != null) {
last_ul.classList.remove('fadeInNote');
}
}
Here we have a tab with incompled tasks and the list is supposed to be attached to the first tab(all-taks). After the task is finished it is supposed to get deleted from first list and move to second one with completed tasks. And after you switch to second tab, all the completed ones should be there. I can't figure out the way to do it.
I hope I could get some help or some detailed explanation, 'cause I have been stuck on this for some time.
Here is an image for more clarification:
[1]: https://i.stack.imgur.com/Dy0KL.png
HTML code:
<body>
<div class="container">
<header>
<h1>To-Do List</h1>
<h4>Describe your list...</h4>
</header>
<form action="" class="todo-form">
<div class="form-wrapper">
<!-- action is where files will be sent after submitting -->
<input class="todo-input" type="text" placeholder="Add a task...">
</div>
<div class="form-wrapper">
<button class="todo-button" type="submit">Add</button>
</div>
</form>
<div class="todo-tabs">
<ul>
<li class="all-tasks active">
<span></span>All tasks (<span class="counter">0</span>)</span>
</li>
<li class="completed">
<span>Completed (<span class="counter">0</span>)</span>
</li>
</ul>
</div>
<div class="todo-list">
<div class="tabs-content" data-tab="1">
<ol class="undone-tasks"></ol>
</div>
<div class="tabs-content" data-tab="2">
<ol class="done-tasks"></ol>
</div>
</div>
</body>
JS code:
//Selectors
const todoForm = document.querySelector('.todo-form');
const todoInput = document.querySelector('.todo-input');
const todoButton = document.querySelector('.todo-button');
const tabs = document.querySelectorAll('.todo-tabs ul li');
const tabWrap = document.querySelector('.todo-tabs ul');
const undone = document.querySelector('.undone-tasks');
const done = document.querySelectorAll('.done-tasks');
//Event Listeners
tabWrap.addEventListener('click', tabs)
todoButton.addEventListener('click', addToDo);
//Functions
tabs.forEach(function (tab, tab_index) {
tab.addEventListener("click", function () {
tabs.forEach(function (tab) {
tab.classList.remove("active");
})
tabWrap.forEach(function (todoList, todoList_index) {
if (todoList_index == tab_index) {
todoList.style.display = "block";
}
else[
todoList.style.display = "none"
]
})
})
})
function addToDo(event) {
//Prevent form from submitting
event.preventDefault();
if (todoInput.value != "") {
const todoDiv = document.createElement('div');
todoDiv.classList.add('todo-div');
const inputCheckbox = document.createElement('input');
inputCheckbox.classList.add("checkbox-incompleted");
inputCheckbox.setAttribute('type', 'checkbox');
todoDiv.appendChild(inputCheckbox);
//Create li
const newToDo = document.createElement('li');
newToDo.classList.add('todo-item');
newToDo.insertAdjacentText("beforeend", todoInput.value);
todoDiv.appendChild(newToDo);
console.log(newToDo)
//Append to list
undone.appendChild(todoDiv);
//Clear todo input value
todoInput.value = "";
//Focusing after 1st input
todoInput.focus();
}
}
The commented area is what i tried to do for tabs and lists to switch, but it gives the following error:
Uncaught TypeError: tabWrap.forEach is not a function
at HTMLLIElement.<anonymous> (script.js:32:17)
(anonymous) # script.js:32
The error starts at the beggining of the tabWrap.forEach function. But I guess there could be other way to solve this
tabWrap is not useful for what you want: it is a single element. Instead, you'll want to iterate over the tab contents, which are identified by class tabs-content. It is those that you need to iterate and show or hide.
Then, to decide which contents to use, you may need to use that data-tab attribute you have in your HTML (not sure, since it is nowhere referenced).
Anyway, adapt as needed:
const tabContents = document.querySelectorAll('.tabs-content');
tabs.forEach(function (activeTab, activeIndex) {
activeTab.addEventListener("click", function (e) {
tabs.forEach(function (tab) {
tab.classList.toggle("active", tab == activeTab);
});
tabContents.forEach(function (tabContent) {
tabContent.style.display = tabContent.dataset.tab == activeIndex + 1 ? "" : "none";
});
})
})
As #Anurag Srivastava pointed out on comment, in your code this is how you get tab elements:
const tabs = document.querySelectorAll('.todo-tabs ul li');
const tabWrap = document.querySelector('.todo-tabs ul');
The method querySelector returns a single element, and querySelectorAll returns an iterable collection.
And this is why you're getting the error:
Uncaught TypeError: tabWrap.forEach is not a function
If you don't have to support internet explorer you could take a look at Element.insertAdjacentElement() to move elements from one tab to another.
I'm working on a recipes page where you have a series of buttons and posts that are interconnected. The buttons have names of recipe categories such as Pie and Cake. When you click on a 'Pie' button, you are only shown the posts that are categorized as 'Pie'. Both the buttons and the posts have data attributes that have their recipe category on there.
I am able to get this to work, however, I'm having issues for when you click on a recipe category button, and there are no corresponding posts. For this, I created an empty '#message' div that would output a message if there were no recipe posts found, and an empty string if there were recipe posts.
When I click on a recipe button that does have posts, I get the 'No Recipes' text in the message. Also weird that it looks like it's applying the correct message only to the last button/post which in this example is 'Cake'.
Can someone explain why this is not working? I get it's probably a scope/closure issue, but I'm unsure what's going on.
//BUTTONS
<section>
<button class="recipe_button" data-btncategory="Pie">
Pie
</button>
<button class="recipe_button" data-btncategory="Cake">
Cake
</button>
</section>
//POSTS
<div id="message"></div>
<section class="recipe" data-postcategory="Pie">
<h2>Pie Recipe</h2>
</section>
<section class="recipe" data-postcategory="Cake">
<h2>Cake Recipe</h2>
</section>
let posts = document.querySelectorAll(".recipe");
let postsArr = Array.from(posts);
let btn = document.querySelectorAll(".recipe_button");
let btnArray = Array.from(btn);
let message = document.getElementById("message");
btnArray.forEach((button) => {
button.onclick = (el) => {
let match = el.target.dataset.btncategory;
postsArr.filter(function(post, i) {
if (post.dataset.postcategory == match) {
posts[i].style.display = "grid";
<-- message not working properly -->
message.innerHTML = "";
} else {
posts[i].style.display = "none";
<-- message not working properly -->
message.innerHTML = "Sorry No Recipes Available";
}
});
}
});
look at how your filter is running. you'll always get ones that match and ones that don't - so both the if and the else code will always run
What you want to do is hide/display posts in the filter, returning true for displayed and false when hidden
That way, the resulting array length will be 0 if no match, and 1 or more if there is a match
Then another if/else after determining if there is anything displayed to show/hide the message
let posts = document.querySelectorAll(".recipe");
let postsArr = Array.from(posts);
let btn = document.querySelectorAll(".recipe_button");
let btnArray = Array.from(btn);
let message = document.getElementById("message");
btnArray.forEach((button) => {
button.onclick = (el) => {
let match = el.target.dataset.btncategory;
let found = postsArr.filter(function(post) {
if (post.dataset.postcategory == match) {
post.style.display = "grid";
return true;
} else {
post.style.display = "none";
return false;
}
}).length;
message.innerHTML = found ? "" : "Sorry No Recipes Available";
}
});
<section>
<button class="recipe_button" data-btncategory="Pie">
Pie
</button>
<button class="recipe_button" data-btncategory="Cake">
Cake
</button>
</section>
//POSTS
<div id="message"></div>
<section class="recipe" data-postcategory="Pie">
<h2>Pie Recipe</h2>
</section>
<section class="recipe" data-postcategory="Cake">
<h2>Cake Recipe</h2>
</section>
Having said all that, the message would NEVER display Sorry No Recipes Available since your buttons guarantee that there will be one displayed
Here's a straightforward way to make your idea work.
It uses an event listener with event delegation.
See the in-code comments for further clarifications.
// Identifies DOM elements
const
btnsDiv = document.getElementById("btns"),
posts = [...document.getElementsByClassName("recipe")],
message = document.getElementById("message");
// Calls `filterPosts` when btnsDiv is clicked
btnsDiv.addEventListener("click", filterPosts);
// Defines `filterPosts`
function filterPosts(event){
// Ignores irrelevant clicks
if(!event.target.classList.contains("btn")){ return; }
// Shows message while there is no match
let match = false;
message.classList.remove("hidden");
// Remembers category
const category = event.target.dataset.category;
// Iterates through recipes
posts.forEach( (post) => {
// Hides recipe until it matches
post.classList.add("hidden");
// If recipe matches, shows it and notes the match
if(post.dataset.category == category) {
post.classList.remove("hidden");
match = true;
}
});
// If any match occurred, hides message
if(match == true){
message.classList.add("hidden");
}
}
.hidden{ display: none; }
<div id = "btns">
<button class="btn" data-category="Pie">Pie </button>
<button class="btn" data-category="Cake"> Cake </button>
<button class="btn" data-category="Pasta"> Pasta </button>
</div>
<div id="message" class="hidden">Sorry No Recipes Available</div>
<div class="recipe hidden" data-category="Pie">
<h2>Pie Recipe 1</h2>
</div>
<div class="recipe hidden" data-category="Cake">
<h2>Cake Recipe 1</h2>
</div>
<div class="recipe hidden" data-category="Cake">
<h2>Cake Recipe 2</h2>
</div>
I am trying to dynamically load a bunch of posts from a API and then implement a like button for each of them.
function load_allposts(){
fetch("/posts")
.then(response => response.json())
.then(posts => {
var enc = document.createElement('div');
enc.className = "post-enc";
let s = ``;
posts.forEach(element => {
s += `<div class="p-container">
<div>
<button type="button" class="btn btn-link" class="profile-btn" data-id=${element[0].author_id}> ${element[0].author_name} </button>
</div>
<div class="post-body">
${element[0].body}
</div>
<div class="p1">
<span class="like-status">${element[0].likes}</span> people like this
<button class="like-btn">${element[1]}</button>
</div>
<div class="post-time">
${element[0].timestamp}
</div>
</div>`;
});
enc.innerHTML = s;
document.querySelector('#all-posts').appendChild(enc);
});
}
I would to like to modify the <span class="like-status"> element when I click the <button class="like-btn">. The only way that I can think of to get a reference to <span class="like-status"> is by adding a ID to it by implementing some kind of counter, which I feel is more like a hack rather than real solution.
I tried googling but almost all solutions involved JQuery, which I am not familiar with. Any help would be appreciated.
You can use delegate event binding document.addEventListener('click', function(event) { to trigger click event for dynamically added button.
It will raise click on every element inside document you need to find if it is one which you expect with event.target.matches('button.like-btn').
Then you can find your span with getting parent and then finding span.like-status using querySelector.
Try it below. For demo modified load_allposts. You do not need to do any change in it.
load_allposts();
document.addEventListener('click', function(event) {
if (event.target.matches('button.like-btn')) {
let span = event.target.parentElement.querySelector('span.like-status');
span.innerText = 'Modified';
}
});
function load_allposts() {
let posts = [1]
var enc = document.createElement('div');
enc.className = "post-enc";
let s = ``;
posts.forEach(element => {
s += `<div class="p-container">
<div>
<button type="button" class="btn btn-link" class="profile-btn" data-id=element[0].author_id> element[0].author_name </button>
</div>
<div class="post-body">
element[0].body
</div>
<div class="p1">
<span class="like-status">element[0].likes</span> people like this
<button class="like-btn">element[1]</button>
</div>
<div class="post-time">
element[0].timestamp
</div>
</div>`;
});
enc.innerHTML = s;
document.querySelector('#all-posts').appendChild(enc);
}
<div id='all-posts'>
</div>
Note event delegation have extra overhead so alternatively you can use below code.
Here added two functions added as below and added one line bindClickEvent(enc); at end of load_allposts function.
likeClick - perform custom logic to update span.like-status
bindClickEvent - bind click event to all button.like-btn inside div
Call bindClickEvent(enc); at end of load_allposts function.
Try it below.
load_allposts();
// perform custom logic to update span.like-status
function likeClick(event) {
// querySelector will return first matching element
let span = event.target.parentElement.querySelector('span.like-status');
span.innerText = 'Modified';
}
// bind click event to all button.like-btn inside div
function bindClickEvent(enc) {
// querySelectorAll will return array of all matching elements
let buttons = enc.querySelectorAll('button.like-btn');
// loop over each button and assign click function
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = likeClick;
}
}
function load_allposts() {
let posts = [1]
var enc = document.createElement('div');
enc.className = "post-enc";
let s = ``;
posts.forEach(element => {
s += `<div class="p-container">
<div>
<button type="button" class="btn btn-link" class="profile-btn" data-id=element[0].author_id> element[0].author_name </button>
</div>
<div class="post-body">
element[0].body
</div>
<div class="p1">
<span class="like-status">element[0].likes</span> people like this
<button class="like-btn">element[1]</button>
</div>
<div class="post-time">
element[0].timestamp
</div>
</div>`;
});
enc.innerHTML = s;
document.querySelector('#all-posts').appendChild(enc);
// assign click event to buttons inside enc div.
bindClickEvent(enc);
}
<div id='all-posts'>
</div>
This is my HTML(list of people)
<button class="btn btn-primary btn-block" id="add_user">Add new user</button>
<ul id="users_list" contenteditable="true">
<li class="user_data">
<img src="http://img.dunyanews.tv/blog_user_images/anchors//15_user_icon.png" alt="44user" class="user_image" />
<div class="user_data_id text-left">Id:></div>
<div class="user_data_username text-left">Username:</div>
<div class="user_data_firstname text-left">Firstname:</div>
<div class="user_data_lastname text-left">Lastname:</div>
<button class="btn btn-danger delete_user">X</button>
</li>
</ul>
This is my Javascript (one function for adding and one for deleting). First work well, but second (deleting) - delete only first (or [0]) element
var addUser = document.getElementById('add_user'),
usersList = document.getElementById('users_list'),
userTemplate = document.getElementsByClassName('user_data');
deleteUser = document.getElementsByClassName('delete_user');
//Function for adding elements
addUser.addEventListener('click', (function() {
var newUser = userTemplate[0].cloneNode(true);
usersList.appendChild(newUser);
}));
//Function for deleting element
for (i = 0; i < deleteUser.length; i++) {
(function(e) {
deleteUser[e].addEventListener('click', (function() {
usersList.removeChild(userTemplate[e]);
}));
})(i);
}
Try this code:
I changed the display on the default template to hidden so we will also have one user to clone from. Otherwise, the user can delete all and we have nothing to clone from.
<button class="btn btn-primary btn-block" id="add_user">Add new user</button>
<ul id="users_list" contenteditable="true">
<--! This display none is important -->
<li class="user_data" style="display: none;">
<img src="http://img.dunyanews.tv/blog_user_images/anchors//15_user_icon.png" alt="44user" class="user_image" />
<div class="user_data_id text-left">Id:></div>
<div class="user_data_username text-left">Username:</div>
<div class="user_data_firstname text-left">Firstname:</div>
<div class="user_data_lastname text-left">Lastname:</div>
<button class="btn btn-danger delete_user">X</button>
</li>
</ul>
To make sure we are deleting the correct user, I added an array of names for testing.
var names = ['Noah', 'Sophia', 'Liam', 'Emma', 'Jacob', 'Olivia', 'Mason', 'Isabella', 'William', 'Ava', 'Ethan', 'Mia', 'Michael', 'Emily', 'Alexander', 'Abigail', 'Jayden', 'Madison', 'Daniel', 'Elizabeth'];
var addUser = document.getElementById('add_user'),
usersList = document.getElementById('users_list'),
userTemplate = document.getElementsByClassName('user_data');
deleteUser = document.getElementsByClassName('delete_user');
//Function for adding elements
addUser.addEventListener('click', (function() {
var newUser = userTemplate[0].cloneNode(true);
// add name to user so we can test.
newUser.getElementsByClassName('user_data_firstname')[0].innerHTML += ' ' + names.pop();
// make this user visible
newUser.style.display = '';
// Wire click event to remove this user.
newUser.getElementsByClassName('delete_user')[0].addEventListener('click', (function() {
usersList.removeChild(newUser);
}));
usersList.appendChild(newUser);
}));
// Add one user
addUser.click();
Note: You start with one user and you loop through the user list to add the remove, so when new users are added, the click to remove event is not wired.
I removed the loop and wire it as we add a new user.
Obviously our code will fail if we click on add users and we run out of names, but that's enough for our test.