Can't print correct innerHTML message within a nested function - javascript

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>

Related

<div> not resettting after event listener function

First time posting here. I have a doubt regarding a website I'm developing for a JS course. I've created a container, and with a function I'm printing a filtered array. This is working just fine, the search is filtering the cards correctly. Problem is, when I delete the text in the input, the div does not reset, and on the contrary, it prints every object into the html, as if the input assumes an empty strings is coincidence enough for the .includes() method.
Here are the functions i'm using for the event listener:
//SEARCH BAR
`searchBar.addEventListener("input", () => { //searchBar has the input tag taken from HTML
search(searchBar.value, cursos)
})`
//FILTER THE ARRAY OF COURSES
`function search (cursoBuscado, array) {
let busqueda = array.filter(
curso => curso.id.toLowerCase().includes(cursoBuscado.toLowerCase()) || curso.duration.toLowerCase().includes(cursoBuscado.toLowerCase())
)
if (busqueda.length == 0) {
coincidencia.innerHTML = `<h1 class="text-center display-5 m-auto">This product is not in our list</h1>`
} else if (cursoBuscado.value === "") {
coincidencia.innerHTML = `<h1 class="text-center display-5 m-auto">This product is not in our list</h1>`
} else {
coincidencia.innerHTML = ""
coincidencias(busqueda)
}
}`
//PRINTING IT TO THE HTML
function coincidencias (array) {
coincidencia.innerHTML = "" //"coincidencias" is the father <div> for every child div.card this function prints to the html, this bit is not working
for (let curso of array) {
let newCoincidencia = document.createElement("div")
newCoincidencia.className = "card m-2"
newCoincidencia.id = `${curso.id}`
newCoincidencia.innerHTML =
`<div class="card-body d-flex flex-column align-items-center m-2">
<p class="card-text">${curso.id}</p>
<h2 class="card-title">${curso.duration}</h2>
<h1 class="card-text display-1">€${curso.price}</h1>
<button type="button" class="btn btn-outline-primary w-100" id="addCart${curso.clase}">Add to cart</button>
</div>`
coincidencia.appendChild(newCoincidencia)
}
}
I want the "coincidencias" to reset, i.e., be empty when the value of the input is empty (""). Instead it prints every element of the "cursos" array.

Delete a product on cart and localStorage

I'm currently on student project where I'm stuck on delete product on cart page I have no problem to delete them on the front page but when it come to remove it too on localStorage honestly I don't know what to do.
I know that using localStorage.setItem allow to update it when necessary but on the code that I wrote I don't know where to put correctly.
I wrote this :
// Targeting arrays
let deleteButton = document.querySelectorAll('.deleteItem');
let localStorageProducts = localStorage.getItem('Produits');
for (let i = 0; i < deleteButton.length; i++) {
// Get all remove buttons
let buttons = deleteButton[i];
// Link to his parent
let myData = deleteButton[i].closest('article');
let getStorageProducts = JSON.parse(localStorageProducts);
buttons.addEventListener("click",() =>
{
getStorageProducts.forEach(localStorageProducts =>{
if(localStorageProducts.id === myData.dataset.id){
// Delete the product
myData.remove();
localStorage.setItem('Produits',(JSON.stringify([localStorageProducts])));
}
})
})
}
<section id="cart__items">
<article class="cart__item" data-id="{product-ID}" data-color="{product-color}">
<div class="cart__item__img">
<img src="../images/product01.jpg" alt="Photographie d'un canapé">
</div>
<div class="cart__item__content">
<div class="cart__item__content__description">
<h2>Nom du produit</h2>
<p>Vert</p>
<p>42,00 €</p>
</div>
<div class="cart__item__content__settings">
<div class="cart__item__content__settings__quantity">
<p>Qté : </p>
<input type="number" class="itemQuantity" name="itemQuantity" min="1" max="100" value="42">
</div>
<div class="cart__item__content__settings__delete">
<p class="deleteItem">Supprimer</p>
</div>
</div>
</div>
</article>
</section>
An example , here I have 4 products : Products in Localstorage
When I click on one of the remove button it's gonna delete 3 of them and one left :
Product left
How could I delete them one by one ?
I refactored your logic in this way (please read comments on commented lines) :
//LOCAL TEST START: i used these lines to test with static local logics you can ignore this
let prods = [{id: 1, prodname: 'prod1'}, {id: 2, prodname: 'prod2'}, {id: 3, prodname: 'prod3'}];
localStorage.setItem('Produits',JSON.stringify(prods));
//LOCAL TEST END.
// i used getElementsByClassName because it's native function which is supported in all browsers even old ones.
let viewArticles = document.getElementsByClassName('cart__item');
let localStorageProducts = localStorage.getItem('Produits');
let products = JSON.parse(localStorageProducts);
// looping on Articles instead of Buttons to be able to get product ID easily
for (let article of viewArticles) {
article.addEventListener("click",function (ev)
{
// by {capturing: true}, i can access to the high clicked element
// (which is the <article> tag in our case) by .currentTarget property
const currentArticle = ev.currentTarget;
// by {capturing: true}, i can access to the exactly clicked child element
//(which is the delete button in this case by performing a check test using .target property class
const isDeleteBtnClicked = ev.target.classList.contains('deleteItem');
// if the child element is the delete button then
// delete the product and update the local storage
if(isDeleteBtnClicked){
// access to product id of the article
const productId = currentArticle.dataset.id;
products = products.filter(prd => prd.id.toString() !== productId.toString());
localStorage.setItem('Produits',JSON.stringify(products));
}
}, {capture: true}); // enable capturing instead of bubbling (top to bottom propagation)
}
for more informations about Capturing and Bubbling you can check this

Trying to move completed tasks from one list to another

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.

How do I use For Loop in JavaScript to show the list?

I am a beginner in JavaScript and I can't figure out the following problem: I am trying to create a simple JavaScript Movie List. I have 10 lists on the Movie List. I tried to show all of the lists with for loop, but it doesn't work.
Here's the code:
function renderModal() {
for (let i = 0; i < listMovies.length; i++) {
let movieData = listMovies[i];
document.getElementById("poster").src = movieData.img;
document.getElementById("title").innerHTML = movieData.name;
document.getElementById("genre").innerHTML = movieData.genre;
document.getElementById("rating-num").innerHTML = "Rating: "+ movieData.rating + "/10";
document.getElementById("movie-desc").innerHTML = movieData.desc;
document.getElementById("imdb-page").href = movieData.link;
return movieData;
}
}
What do I have to do?
Help me to fix it!.
You can use template tag for list and render it into target element.I am showing an example.
Movie list
<div id="movieList"></div>
template for list
<template id="movieListTemplate">
<div class="movie">
<img src="" class="poster" alt="">
<div class="title"></div>
<div class="genre"></div>
<div class="rating-num"></div>
<div class="movie-desc"></div>
<div class="imdb-page"></div>
</div>
</template>
Javascript code:
if (listMovies.length > 0) {
const movileListTemplate = document.getElementById('movieListTemplate')
const movieRenederElement = document.getElementById('movieList')
for(const movie of listMovies) {
const movieEl = document.importNode(movileListTemplate.content, true)
movieEl.querySelector('.poster').src = movie.img
movieEl.querySelector('.title').textContent = movie.name
//use all queryselector like above
}
}
Your return movieData; will stop the loop dead. Not that running it more than once will change anything since you change the same elements over and over. IDs must be unique.
Here is a useful way to render an array
document.getElementById("container").innerHTML = listMovies.map(movieData => `<img src="${movieData.img}" />
<h3>${movieData.name}</h3>
<p>${movieData.genre}</p>
<p>Rating: ${movieData.rating}/10</p>
<p>${movieData.desc}
IMDB
</p>`).join("<hr/>");
With return movieData, the for loop will ends in advance.You should put it outside the for loop.

Can I assign multiple data-id values to one attribute in one element?

I use WordPress. I have an audio player on my site. When a user clicks the play button on an individual song, it will play the song. The post ID is how the player determines which song to play.
So in the DIV that wraps the button tag, I assign an attribute: data-id which is the post id of the song. My app then grabs the MP3 file that is associated with that post ID. Simple.
My play button tag is essentially this:
<div class="item item-action" data-id="1161">
<button class="btn-playpause"></button>
</div>
The important parts of my player.js file that handles the click event and plays the song is below:
Simulate the play button
// simulate the play btn
$(document).on('click.btn', '.btn-playpause, .btn-queque', function(e){
e.stopPropagation();
var self = $(this),
item = $(this).closest('.item'),
id = item.attr('data-id'),
type = item.data('user-id') ? 'user' : 'post',
play = true;
if(!player){
getItem(id, type).done(function(obj){
if(obj.status == 'success'){
mep.mepPlaylistTracks = obj.tracks;
initPlayer();
player && player.mepSelect(0, true);
}
});
return;
}
if(self.is('.btn-queque')){
play = false;
self.parent().dropdown('toggle');
}
if( self.hasClass('is-playing') ){
self.removeClass('is-playing');
player.pause();
}else{
var index = player.find(id);
if( index !== -1){
var track = player.mepGetCurrentTrack();
if(track && track.id == id && !play) return;
player.mepSelect(index, true);
}else{
getItem(id, type).done(function(obj){
if(obj.status == 'success'){
addToPlay(obj.tracks, play);
}
});
}
}
});
getItem() function
function getItem(id, type){
return $.ajax({
type : "post",
dataType : "json",
url : ajax.ajax_url,
data : {action: "ajax_music", id : id, type: type, nonce: ajax.nonce}
});
}
addToPlay() function
function addToPlay(obj, play){
if(obj.length == 1){
player.mepAdd( obj[0], play );
}else if(obj.length > 1){
if(play){
player.options.mepPlaylistTracks = obj;
player.updatemepList();
player.mepSelect(0, true);
}else{
for(var i=0; i<obj.length; i++){
player.mepAdd( obj[i] );
}
}
}
}
You can see that once I click the play button, depending on a condition, it fires the getItem() function. getItem() takes the data-id attribute defined earlier as the id variable and begins to play the song.
The addToPlay() function will add another song to the playlist if there is already an instance of the player open. And when a user clicks another play button, it will add that new song to the list and begin to play that song.
Is it possible to modify this code so that I can add multiple data-id values to my button code so that my script will recognize there is more than one song id and add those to my playlist, using the addToPlay() function?
I'd like to do something like this
<div class="item item-action" data-id="1161, 1288, 1456">
<button class="btn-playpause"></button>
</div>
The reason I want to do this, is I would like to add a feature that will allow my users to Play All Songs on the current page they're viewing. Some of our pages, like the catalog page, displays dozens of songs that fit a certain criteria and I'd like the option to play all songs on the page.
Can anyone help push me in the right direction?
Thanks.
You could store a JSON encoded array in the data-id tag. Like:
<div class="item item-action" data-id='[1161, 1288, 1456]'>
<button class="btn-playpause"></button>
</div>
Then you can parse the array, and loop over its contents, adding each id to the queue.
See an example of parsing here: https://gist.github.com/charliepark/4266921
Here I present a alternative method.
In your markup, you can set the data-id='[1234] attribute as an encoded array. You can then collect those properties and set them as the data id property on a given element using jQuery as I illustrate here:
let someElement = $('.item');
let setItems = someElement.data('id');
let thisItem = someElement.attr('data-id');
IMPORTANT: the someElement.data('id') returns the same as the someElement.attr('data-id') (except as a string) IF nothing for that element has been set with someElement.data('id',[567,890]); which sets the property id for someElement. So, once set that way
let x = someElement.data('id'); // returns the array 567,890
let org = someElement.attr('data-id'); // still returns the [1234]
This code illustrates:
let someElement = $('.item').first();
let setItems = someElement.data('id');
let thisItemS = someElement.attr('data-id');
// make an array of the string
let thisItem = JSON.parse(thisItemS);
console.log(typeof setItems, setItems, typeof thisItemS, thisItemS, typeof thisItem, thisItem);
someElement.data('id', [567, 890]);
let x = someElement.data('id'); // returns and gets the array 567,890
let org = JSON.parse(someElement.attr('data-id')); // still returns the string [1234] made into an array
// merge with no duplicates, in the other code I used .map()
let z = Array.from(new Set(x.concat(org)));
console.log(x, org, z);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<div class='item' data-id='[1234]'>me the item</div>
This code show a set of elements using the concepts introduced above plus:
Set some check boxes for selection
Get the values (all)
Get the values (checkbox checked)
You can combine any of this for your original purpose, but you will need to manage that addition of new items values (and subsequently how to remove perhaps) on a given element. You might even add to a <select> element so the user then can pick some to remove perhaps...
function showPicks(choicedata) {
let ul = $('#choices').append('<ul />');
//ul.remove('li');
ul.html('empty');
ul.text('choices:');
$.each(choicedata, function(index, choice) {
let c = $('<li/>').html(choice);
ul.append(c);
});
}
$(function() {
let mydata = $('.item').map(function() {
return $(this).data('id');
}).get();
$('.all-items').data("id", mydata);
showPicks(mydata); //now do what is desired
});
$('item.item-action').filter(':not(".some-items")')
.on('click', '.btn-playpause', function() {
let setItems = $(this).closest('.item').data('id');
let thisItem = $(this).closest('.item').attr('data-id');
// do what you wish here, I show them
showPicks(setItems);
});
$('.some-items').on('click', '.btn-playpause', function(event) {
let mydata = $('.item-select').filter(':checked')
.add(this) // set an additional one from the button
.closest('.item')
.map(function() {
return $(this).data('id');
}).get();
$(event.delegatedTarget).data("id", mydata);
let setItems = $(event.delegatedTarget).data('id');
// the original pick
let thisItem = $(event.delegatedTarget).attr('data-id');
// do what you wish here
showPicks(mydata);
});
.btn-playpause {
height: 2em;
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<div class="item item-action" data-id="[1161]">
<button class="btn-playpause" type="button"></button>
<input type="checkbox" class="item-select" />
</div>
<div class="item item-action" data-id="[1163]">
<button class="btn-playpause"></button>
<input type="checkbox" class="item-select" />
</div>
<div class="item item-action" data-id="[9961,7744]">
<button class="btn-playpause" type="button"></button>
<input type="checkbox" class="item-select" />
</div>
<div class="item item-action all-items" data-id="[42]">
<button class="btn-playpause" type="button">Play All</button>
</div>
<div class="item item-action some-items" data-id="[95]">
<button class="btn-playpause" type="button">Play selected</button>
</div>
<div id="choices">none</div>

Categories