Trying to remove menu visibility when item is clicked - javascript

I'm trying to make a responsive menu in a React app, which appears when I click on menu title and disappears when I click on an item, but it seems to not respond really well, Menu disappears sometimes, sometimes it doesn't, it seems like a bug (E.g it always bug the first time I click on an item after refreshing the page)
Edit: the setAside function replaces the Title value with the clicked value, and the title value is added to the reordered list of items, when I click on item that was in the Title last time, classList.remove doesn't work...
this is my code:
const MyComponent = (props) => {
const [title, setTitle] = useState(props.name);
const [lastTitle, setlastTitle] = useState(props.number);
const [listItem, setListItem] = useState(props.list);
const list = (listofItems) => {
const item = [];
listofItems.forEach((value, index) => {
item.push(<li onClick={() => setParam(index, value)} className="items">Item {value}</li>)
});
return item;
}
// This function changes the value of Title to the clicked item value and reorders the values
const setAside = (value) => {
setTitle('Item ' + value);
let newListItem = listItem.filter((item) => item !== value);
newListItem.push(lastTitle);
newListItem.sort(function(a, b){ return a - b });
setlastTitle(value);
setListItem(newListItem);
}
// receives a new page from another component
const setPage = (index, value) => {
for (let x=0; x<3; x++) {
if (listItem[index] == value) {
props.Page("item" + value);
}
}
}
// This is the function called when I click an item
const setParam = (index, value) => {
setAside(value);
setPage(index, value);
}
// Make my menu visible
const mouseClickItem = () => {
var menuItem = document.querySelector(".item-list");
menuItem.classList.toggle("item-responsive");
}
var menuItem = document.querySelector(".item-list");
var items = document.querySelectorAll(".items"); // <---- This var doesn't seems to get the value correctly, but I don't know why
// trying to remove menu visibility when I click on an item
items.forEach(n => n.addEventListener("click", () => {
menuItem.classList.remove("item-responsive"); // <---- not removing items with every click but in some
}))
return (
<aside className="item-choice">
<div onClick={mouseClickItem}> {title} </div>
<ul className="item-list" type="none">
{list(listItem)}
</ul>
</aside>
)
}
Does anyone have any other ideas on how I could remove the menu on click?

I found a solution, I just define menuItem and removed the class inside setAside like this:
const setAside = (value) => {
var menuItem = document.querySelector(".item-list");
...
menuItem.classList.remove("item-responsive");
}
so there is a problem removing the class using forEach or addEventListener, although I don't know what exactly it is.

Related

How to count for each mapped element?

Solved - wasn't aware of the useRef hook which helped me track each individual mapped item.
I have a set of results mapped out within a card element. I want to keep a click count for each of those elements, but with a global JS variable, it counts the clicks of all elements if I call that variable on more than one clickable element per session. I have tried to do id.index, adding (id) + index etc but am stumped. How do I properly use the unique id's to track the index for each card? Thanks
function onClick(id) {
let index = 0;
index++;
if (index >= 1) {
dosomething
} else if (index === 0) {
dosomethingelse
}
}
It's not clear what and how you want to count and onclick events.
Assuming that you need to keep track of clicks on each element/id:
You can use the useRef hook and keep it a global object to track the number of clicks per id.
const clicksPerId = useRef({});
function onClick(id) {
if (!clicksPerId.current[id]) {
clicksPerId.current[id] = 0;
}
clicksPerId.current[id]++;
// whatever you want to do with the clicks count
}
I'm kinda confused by your question to be honest however for working with arrays in javascript / React maybe you'll find some of these helpful
Getting the array length
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
// find the length of the array
const getArrayLength = () => {
return myArray.length;
}
return (
<p>hello there</p>
)
}
Doing something with the index of a maped component:
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
const handleClick = (index) => {
// do somthing with the index of the el
};
return (
<>
{ myArray.map((el, index) => {
return (
<p
key={index}
onClick={() => handleClick(index)}
>
el number { el }
</p>
)
})
}
</>
)
}

Template Component doesn't re-render when it is removed from array

Everything works fine when I append the template onclick. However when the remove events fire it deletes it from the array. It deletes it from the array but when I go to click to append it back to the array, an error fires off. Cannot read property 'content' of null at HTMLButtonElement.. I've double checked my Html and that seems fine. I don't know why it won't re-append or re-render after it was already pushed into the array and then deleted.
// Function to remove item from array onClick
function removeItem(shoppingItems, btns) {
btns.addEventListener('click', (e) => {
let newArr = shoppingItems.filter(item => item.id !== item.id)
shoppingList = newArr
cartMenuItems.innerHTML = newArr
cartItemCount.innerText = newArr.length
return newArr
})
}
// Template Component
btn.addEventListener('click', (e) => {
if (shoppingList.indexOf(cartItem) !== -1) {
return
}
else {
const menuTemplate = document.querySelector('#menu-template').content
const copyMenuTemplate = document.importNode(menuTemplate, true);
copyMenuTemplate.querySelector('.menu-item-title').textContent = cartItem.title;
copyMenuTemplate.querySelector('.menu-price').textContent = cartItem.price;
copyMenuTemplate.querySelector('.menu-img').src = cartItem.image;
const removeBtn = copyMenuTemplate.querySelector('.remove-btn')
cartMenuItems.appendChild(copyMenuTemplate);
shoppingList.push(cartItem)
cartItemCount.innerText = shoppingList.length;
removeItem(shoppingList, removeBtn);
}
I figured it out. I removed the the original menu template from the else statement. I was deleting the model template every time I clicked the button so it couldn't re-render null.

useState updates correctly when adding to array, but not removing

I have a simple component that allows me to select an item from a list, then remove an item from a list. I display the active list within a parent component. No matter what I do or how I approach it, the removal of an active component is never updated unless they are all in active.
Here is a smaller (yet large snippet) of how it is setup. Below it I describe where I found to be the problem:
const Viewer = () => {
const [items, setItems] = useState(["inactive"]);
return (
<ItemSelect setItems={setItems} selected={items}/>
<DisplayItems items={items}/>
)
}
const ItemSelect = ({setItems, selected}) => {
const handleActiveItems = (activeItems) => {
setItems(activeItems);
}
return (
<SelectItems
handleActiveItems={handleActiveItems}
items={selected}
/>
)
}
const SelectItems = ({handleActiveItems, items}) => {
const [selected, setSelected] = useState([])
useEffect(() => {
setSelected(items);
}, [items]);
const randomTestItem = ["apple", "peach", "orange"];
const handleOnClick = (isSelected, item) => {
let tmpItems = items;
if (isSelected) {
let index = tmpItems.indexOf("inactive");
if (index > -1) {
handleActiveItems([option]);
} else {
handleActiveItems([...selected, option]);
}
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
}
return (
{
randomTestItem?.map((item,index) => {
return (
<DisplayClickable item={item} onClick={handleOnClick} key={index}/>
)
})
}
)
}
<DisplayClickable item={item} onClick={handleOnClick}/> holds a useState() that toggle from active/inactive.
I've tested this in many different area's I believe the crux of the problem to be here:
} else if (!isSelected) {
let index = tmpItems.indexOf(option);
if (index > -1) {
tmpItems.splice(index, 1);
if (tmpItems.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems([tmpItems]);
}
}
}
specifically:
} else {
handleActiveItems([tmpItems]);
}
When I unselect all the items and switch the array back to "inactive", everything updates instantly and exactly how you would expect. Selecting items always adds to the list correctly, it's removing them that everything goes wonky. I've done a console.log right before calling handleActiveItems() and the tmpItems array is always correct to what it should be. It just never updates the set state.
Within handleActiveItems the log also shows it is receiving the array just before setting it. It just never sets it.
I believe since you are using the splice method, you just modify the existing array and React does not recognize it as "updatable". You can try to use the filter method:
if (index > -1) {
const newArray = tmpItems.filter((_, itemIndex)=> itemIndex !== index)
if (newArray.length === 0) {
handleActiveItems(["inactive"]);
} else {
handleActiveItems(newArray);
}
}
With the code above, filter method will generate a new array.
Give it a try, hopefully it will help =)
update
I've just realized, maybe you don't need the extra [] you are putting into handleActiveItems(). So instead of:
handleActiveItems([tmpItems])
It could be just:
handleActiveItems(tmpItems)
I figured it out.
It all came down to this line:
let tmpItems = items;
Changing to this:
let tmpItems = [...items];
for some reason allowed React to pay more attention and notice that there was in fact a change.
I just changed in my development build and it works without a hiccup.

React: setState doesn't re-render the component

I'm implementing a shopping cart for a ecommerce website. The shopping cart is a state variable shopCart represented by an array of objects. Each object contains information about a product, such as title and price. I am trying to implement a remove button, which is actually doing what is intended from it, which is to remove items from the shopCart state, but the changes are not represented on the screen render. I can empty the cart, but the screen still shows the products. Here is the main code of the shopping cart page:
return (
<div class={styles.container}>
<h1>Product</h1><h1>Quantity</h1><h1>Unit price</h1><h1>Total price</h1><div></div>
{
shopCart.map((product, i, array) => <CartItem key={product.id} product={product} index={i} array={array}/>)
}
</div>
)
And here is the implementation of CartItem.js
const CartItem = (props) => {
let { shopCart, setShopCart } = useContext(Context);
let product = props.product;
// takes the identification of a shopping cart product and removes it from the cart
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart;
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={() => {
decrease(product.id);
}}>Remove</button>
</div>
)
}
Why isn't it rendering the cart correctly, even though the cart items are being removed after each click of the remove button ?
Issue
You are mutating state. You save a reference to state, mutate it, then save it back into state, so the array reference never changes. React uses shallow equality when checking if state or props update.
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart; // <-- Saved state ref
aux.splice(i, 1); // <-- mutation
setShopCart(aux); // <-- Saved ref back to state
}
}
})
}
Solution
The correct way to update arrays in react state is to copy the array elements into a new array reference. This can be easily accomplished by filtering the current cart by item id. I also suggest changing the argument name so it is clearer what it represents.
const decrease = (id) => {
setShopCart(shopCart => shopCart.filter(item => item.id !== id));
}
You're modifying the shopCart (aux is a reference) directly which is both the context and the collection that you're iterating over. You need to make sure you're updating a copy of the shopping cart and resetting the context. Minimally, you can do the following:
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart.slice(); // makes a copy
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
However, I suggest using the approach Drew recommended. The current approach isn't ideal.
The solution is much simpler than you think. You can use array.filter to remove the matching product by id.
const CartItem = (props) => {
const { product} = props;
let { shopCart, setShopCart } = useContext(Context);
// takes the identification of a shopping cart product and removes it from the cart
const handleClick = (e) => {
const filteredShopCart = shopCart.filter(item => item.id !== product.id);
setShopCart(filteredShopCart);
};
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={handleClick}>Remove</button>
</div>
);
};

View not updating on state change

So I got a list of buttons that looks like this
The functionality that I aim for is when you press a button its background will change to another color.
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
I use this function to return a list of selected items. Currently I'm only returning one item but I made it an array so I can handle multiple items in the future.
In the render function I have something like this:
<View style={feed_back_page_styles.buttons_wrapper}>
{
feedbackButtons.map((item, i) => (
<TouchableOpacity style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.pressedStyle:feed_back_page_styles.inputStyle}
onPress={()=>this.onButtonPress(item.key)}>
<Text style={this.state.selectedItems.includes(item.key)?feed_back_page_styles.option_text_style_pressed:feed_back_page_styles.option_text_style}>{item.data}</Text>
</TouchableOpacity>
))
}
</View>
feedbackButtons is just an array with a key and text.
The onButtonPress method looks like this:
onButtonPress = (key) =>{
updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems,key);
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
The problem is that the view does not update on state change. When I click the button the state gets updated but the view stays the same.
I think this is wrong
const getUpdatedSelectedItemsArray = (selectedItems, id) => {
selectedItems = []
selectedItems.push(id);
return selectedItems;
};
Since you are passing the this.state.selectedItems as 1st argument from your onButtonPress, actually its not creating new array, but using the same reference of state and state should not be modified directly, always use setState().
So basically what you are doing is :
const getUpdatedSelectedItemsArray = (id) => {
this.state.selectedItems = []
this.state.selectedItems.push(id);
return selectedItems;
};
Which is completely wrong and might be the actual issue.
what you can do instead is :
const getUpdatedSelectedItemsArray = (selectedItems=[], id) => {
const items = [...selectedItems]
items.push(id);
return items;
};
and then :
onButtonPress = (key) =>{
const updatedItems = getUpdatedSelectedItemsArray(key); // since currently you want to keep only 1 item in the list
/* Incase more than 1 items, you can then use this
const updatedItems = getUpdatedSelectedItemsArray(this.state.selectedItems, key);
*/
this.setState({selectedItems:updatedItems},()=>console.log(this.state.selectedItems));
console.log("Do smth else here");
}
Hope this resolves your issue.
Also, if you can share your component, it can help if there is some other issue with your component like if you are using PureComponent.

Categories