Why is "onclick" only working once and fails to update? - javascript

I am trying to understand why this onclick button is only working once.
Basically I am testing to see if the "heart" or "wishlist" button is clicked on. When clicked, console.log the name of the product so I can confirm it. But it only picks up the first product. When I click the wishlist button on the second product.
It gives this error "Uncaught SyntaxError: Invalid or unexpected token (at products:1:10)"
When I go to that line it just show ''
I have also tried using a
const wishlistBtn = document.querySelector('.wishlistBtn');
wishlistBtn.addEventListener('click', (product_name) => { console.log(product_name) })
But it just returns that the property is null. I'm wondering if the reason is because of the innerHTML I am including all of this in.
Javascript:
const getProducts = () => {
return fetch('/get-products', {
method: 'POST',
headers: new Headers({'Content-Type':'application/json'}),
body: JSON.stringify({})
})
.then(res => res.json())
.then(data => {
createProductCards(data);
})
}
var wishlist = (product_name) => {
console.log(product_name);
}
const createProductCards = (data) => {
let parent = document.querySelector('.container');
let start = '<div class="product-container">';
let middle = '';
let end = '</div>';
for(let i = 0; i < data.length; i++){
if(data[i].id != decodeURI(location.pathname.split('/').pop()) && !data[i].draft){
middle += `
<div class="product-card">
<div class="product-image">
${data[i].discount === '0' ? ``:`
<span class="discount-tag">${data[i].discount}% off</span>
`}
<img src="${data[i].images[0]}" class="product-thumb" alt="">
<button class="card-btn wishlistBtn" onclick="wishlist('${data[i].name}')"><i class="bi-heart"></i></button>
</div>
<div class="product-info">
<h6 class="product-brand">${data[i].name}</h6>
${data[i].discount === '0' ? `<span class="price">$${data[i].totalPrice}</span>`:`
<span class="price">$${data[i].totalPrice}</span>
<span class="actual-price">$${data[i].actualPrice}</span>
`}
</div>
</div>
`;
}
}
parent.innerHTML = start + middle + end;
}
getProducts();

document.querySelector works only on the first matched element. You may need to use document.querySelectorAll & attach event after the for loop has completely finished it's execution
const wishlistBtn = document.querySelectorAll('.wishlistBtn').forEach((item) => {
item.addEventListener('click', getProductName)
})
function getProductName(product_name) {
console.log(product_name)
})
Here is an example
document.querySelectorAll('.test').forEach(item => {
item.addEventListener('click', getButtonValue)
})
function getButtonValue(elem) {
console.log(elem.target.innerHTML)
}
<button class="test">1</button>
<button class="test">2</button>
<button class="test">3</button>
<button class="test">4</button>
<button class="test">5</button>
<button class="test">6</button>

document.querySelector only returns the first instance of the selector. So the first wish list button on your page is the only one that gets a listener attached.
If you're coming from JQuery, this is a nuanced difference. To add the event listener to every .wishlistBtn you could do something like:
const wishlistBtns = document.querySelectorAll('.wishlistBtn');
[...wishlistBtns].forEach(wishListButton => wishListButton.addEventListener('click', (product_name) => { console.log(product_name) })
There are two differences:
The use of querySelectorAll returns a NodeList of all of the elements that match the .wishlistBtn selector.
Iterate over the NodeList and add an event listener to each individual node. Unfortunately NodeList isn't exactly an array so [...wishlistButtons] is a quick and dirty way to convert it to an array using the relatively new spread operator ...

I seem to have found my problem. The issue was with one of my products having quotations inside of it for some reason but once removed the onclick worked multiple times while sending the product name to a function to keep track.
The problem with the answers given was also that I didnt want to display the name at all inside the button itself <button class=“test”>Item</button> instead this is what I needed <button onclick=‘func(${passname})></button> so that would have not worked when attempted but it gave me a general idea for future references. Thanks!

Related

AddEventListener event fires only once

I am using Django templating engine and JavaScript. My HTML looks like this
<p class="content-card__address">{{ z.formatted_address|truncatewords:6 }}</p>
<div class="content-card-inner">
<p class="content-card__review">Отзывы ({{ z.post_relate.all.count }})</p>
<p class="content-card__phone">{{ z.international_phone_number }}</p>
<div class="div-shaddow"></div>
<p class="content-card__text">Показать</p>
</div>
Cards with text to be generated on the backend using a template engine. My JavaScript code only works on the first card and I need it to work on all cards. With JavaScript I add a class to the div elements. Here is my JavaScript
let call = document.querySelector('.content-card__text');
let divShadow = document.querySelector('.div-shaddow');
call.addEventListener('click', clickCall)
function clickCall() {
call.classList.add('visually-hidden');
divShadow.classList.add('visually-hidden');
}
This code returns you the first element in the DOM and you add click handlers only for it
document.querySelector('.content-card__text')
It will work for you:
const buttons = document.querySelectorAll('.content-card__text');
buttons.forEach(button => {
button.addEventListener('click', clickCall);
});
But please also note that you need to take this into account when working with .divShadow if this element is not alone on the page
Update: example based on your comment
const buttons = document.querySelectorAll('.content-card__text');
const divShadow = document.querySelectorAll('.div-shaddow');
buttons.forEach((button, index) => {
button.addEventListener('click', () => clickCall(index));
});
function clickCall(index) {
buttons[index].classList.add('visually-hidden');
divShadow[index].classList.add('visually-hidden');
}

How to get MDCMenu instance by element?

Let assume that I have a lot of html elements need to use MDCMenu. I don't want to init them one by one, so I init all of them with the code below:
html:
<button class="my-menu-toggle" data-toggle="mdc-menu" data-target="#my-menu">Menu Toggle</button>
<div class="mdc-menu" id="my-menu">
</div>
js:
document.querySelectorAll('[data-toggle="mdc-menu"]').forEach(toggleEl => {
let menuEl = document.querySelector(toggleEl.dataset.target);
let menu = new MDCMenu(menuEl);
toggleEl.addEventListener('click', (e) => {
menu.open = !menu.open;
});
// maybe I should do this, just wondering that if MDC already do same thing that I haven't figure out.
menuEl.MDCMenu = menu;
});
then I want to do somethings with one of menu, how can I get the MDCMenu instance of the element?

Adding or removing from an array based on if it includes event

I am trying to have a single button that will add the value to the array, or if the value already exists in the array to remove that value.
The array is called toppings
const ToppingPlusMinus = (e) => {
if (toppings.includes(e.target.value, 1)) {
removeTopping(e)
}
else {
changeToppings(e);
}
}
The two functions
const removeTopping = (e) => {
toppings.splice(e.target.value)
}
const changeToppings = (e) => {
setToppings([...toppings, e.target.value]);
};
The buttons are toppings on a pizza, and they are in this format
<button
type="button"
value="Sausage, "
className="button btn first"
onClick={(event) => {
ToppingPlusMinus(event);
}}
>
Sausage
</button>
This works fine for adding the value to the array, however on removal there are unexpected outcomes.
the item only is removed after the user double clicks the button
the removal also removes the topping before it in the array.
the button does not stay in its active state while the item is in the array (I haven't attempted this, but if anyone know how to do it I'd be appreciative)
I have set up a codesandbox with my code here
I appreciate your help.
I have made a working example on this code sandbox, so please check it!
The main reason your code doesn't work is the usage of toppings.splice(e.target.value) as here you are trying to directly modify the state without following the proper flow of setting it. So you would need to filter it out properly.
In my example, I used a different approach for the values, but the logic for removing them could remain the same with your approach too.
First, the .includes doesn't need the second argument.
toppings.includes(e.target.value) should be sufficient.
Second, you should define the remove function like this:
const removeTopping = (e) => {
setToppings((toppings) => {
return toppings.filter(topping => topping !== e.target.value)
})
}
This should result in setting your toppings array with a new array that doesn't contain the selected value. You can also pass the value directly to your removeTopping and changeToppings functions.
So basically you have array of items and you want to remove item if it exist.
This is vanilla js solution:
let toppings = []; // this will hold the topping
const ToppingPlusMinus = (e) => { // this will trigger on click
if (toppings.indexOf(e.target.value) > -1) { // if the value of the button exist:
removeA(toppings, e.target.value); // remove it
}
else {
toppings.push(e.target.value); // add it to the array
};
document.querySelector('#toppings').textContent = toppings.toString(); // this for demo
};
function removeA(arr) {
// https://stackoverflow.com/a/3955096/6525081
var what, a = arguments, L = a.length, ax;
while (L > 1 && arr.length) {
what = a[--L];
while ((ax= arr.indexOf(what)) !== -1) {
arr.splice(ax, 1);
}
}
return arr;
}
#toppings {font-size: larger;}
<button type="button" value="Sausage" className="button btn first" onClick="ToppingPlusMinus(event)">Sausage</button>
<button type="button" value="onion" className="button btn first" onClick="ToppingPlusMinus(event)">Onion</button>
<button type="button" value="olive" className="button btn first" onClick="ToppingPlusMinus(event)">Olive</button>
<div id="toppings"></div>

React Hooks: useState with onClick only updating the SECOND time button is clicked?

Edit: forgot an important part - this is noticeable if you click the button next to Jeff A. Menges and check the console log.
The important part of the code is the "setFullResults(cardResults.data.concat(cardResultsPageTwo.data))" line in the onClick of the button code. I think it SHOULD set fullResults to whatever I tell it to... except it doesn't work the first time you click it. Every time after, it works, but not the first time. That's going to be trouble for the next set, because I can't map over an undefined array, and I don't want to tell users to just click on the button twice for the actual search results to come up.
I'm guessing useEffect would work, but I don't know how to write it or where to put it. It's clearly not working at the top of the App functional component, but anywhere else I try to put it gives me an error.
I've tried "this.forceUpdate()" which a lot of places recommend as a quick fix (but recommend against using - but I've been trying to figure this out for hours), but "this.forceUpdate()" isn't a function no matter where I put it.
Please help me get this button working the first time it's clicked on.
import React, { useState, useEffect } from "react";
const App = () => {
let artistData = require("./mass-artists.json");
const [showTheCards, setShowTheCards] = useState();
const [fullResults, setFullResults] = useState([]);
useEffect(() => {
setFullResults();
}, []);
let artistDataMap = artistData.map(artistName => {
//console.log(artistName);
return (
<aside className="artist-section">
<span>{artistName}</span>
<button
className="astbutton"
onClick={ function GetCardList() {
fetch(
`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"`
)
.then(response => {
return response.json();
})
.then((cardResults) => {
console.log(cardResults.has_more)
if (cardResults.has_more === true) {
fetch (`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"&page=2`)
.then((responsepagetwo) => {
return responsepagetwo.json();
})
.then(cardResultsPageTwo => {
console.log(`First Results Page: ${cardResults}`)
console.log(`Second Results Page: ${cardResultsPageTwo}`)
setFullResults(cardResults.data.concat(cardResultsPageTwo.data))
console.log(`Full Results: ${fullResults}`)
})
}
setShowTheCards(
cardResults.data
.filter(({ digital }) => digital === false)
.map(cardData => {
if (cardData.layout === "transform") {
return (
//TODO : Transform card code
<span>Transform Card (Needs special return)</span>
)
}
else if (cardData.layout === "double_faced_token") {
return (
//TODO: Double Faced Token card code
<span>Double Faced Token (Needs special return)</span>
)
}
else {
return (
<div className="card-object">
<span className="card-object-name">
{cardData.name}
</span>
<span className="card-object-set">
{cardData.set_name}
</span>
<img
className="card-object-img-sm"
alt={cardData.name}
src={cardData.image_uris.small}
/>
</div>
)
}
})
)
});
}}
>
Show Cards
</button>
</aside>
);
});
return (
<aside>
<aside className="artist-group">
{artistDataMap}
</aside>
<aside className="card-wrapper">
{showTheCards}
</aside>
</aside>
);
};
export default App;
CodesAndBox: https://codesandbox.io/embed/compassionate-satoshi-iq3nc?fontsize=14
You can try refactoring the code like for onClick handler have a synthetic event. Add this event Listener as part of a class. Use arrow function so that you need not bind this function handler inside the constructor. After fetching the data try to set the state to the result and use the state to render the HTML mark up inside render method. And when I run this code, I have also seen one error in console that child elements require key attribute. I have seen you are using Array.prototype.map inside render method, but when you return the span element inside that try to add a key attribute so that when React diffing algorithm encounters a new element it reduces the time complexity to check certain nodes with this key attribute.
useEffect(() => {
// call the functions which depend on fullResults here
setFullResults();
}, [fullResults])
// now it will check whether fullResults changed, if changed than call functions inside useEffect which are depending on fullResults

error when trying to click on element that was hidden and then made visible again

When I click a button I am using axios to send some data to my node.js backend and when the button is clicked I hide it and show a spinner. If something goes wrong, I hide the spinner and show the buttons again. That works fine. But if I click on the button after it is visible again I get an error that says :
Uncaught TypeError: Cannot read property 'insertAdjacentHTML' of null
at renderLoader (26786sdg72635287hd)
at HTMLAnchorElement.document.querySelector.addEventListener.e
HTML:
<div class="spinner"></div>
JAVASCRIPT
const renderLoader = parent => {
const loader = `
<div class="loader">
<svg>
<use href="/images/icons.svg#icon-cw"></use>
</svg>
</div>
`;
parent.insertAdjacentHTML('afterbegin', loader);
};
const clearLoader = () => {
const loader = document.querySelector('.spinner');
if (loader) {
loader.parentElement.removeChild(loader);
}
};
document.querySelector('.approve').addEventListener('click', e => {
e.preventDefault();
const csrf = document.querySelector('[name=_csrf]').value;
const productId = document.querySelector('[name=productId]').value;
document.querySelector('.approve').style.visibility = 'hidden';
document.querySelector('.reject').style.visibility = 'hidden';
renderLoader(document.querySelector('.spinner'));
axios.post('/account/approve/' + productId, {
status: 'approved'
},
{
headers: {
'csrf-token': csrf
}
})
.then(response => {
const approveBox = document.querySelector('.dashboard-list-box');
const successMessage = document.querySelector('.success');
approveBox.classList.add('fade-out');
successMessage.classList.add('notification');
successMessage.innerHTML = response.data.message;
clearLoader();
})
.catch(err => {
clearLoader();
document.querySelector('.approve').style.visibility = 'visible';
document.querySelector('.reject').style.visibility = 'visible';
});
});
In case of an error, clearLoader is called which removes the .spinner element and I think it shouldn't, to fix this just change this line:
const loader = document.querySelector('.spinner');
to
const loader = document.querySelector('.loader');
from inside the clearLoader function. That seems to have been your intention given the variable's name.
first click you are remove element in dom '.spinner' and After second click your are calling this renderLoader function and you are sending html element as prop so you cant do this because you are remove with this function '.spinner' html element so you need hide element or you need create this element again.
In terms of hiding the specific element like document.querySelector('.approve').style.visibility = 'hidden' that you have, you should look into something like document.querySelector('.approve').classList.add('hide);
also in terms of why insertAdjacentHTML is not working, post it outside of the scope of the const 'renderLoader'.
Also, creating one object that accounts for document.querySelector('.approve') and document.querySelector('.reject')

Categories

Resources