I'm creating navigation for mobile devices. Everything works, but when I'm trying to open third level of navigation, it doesn't open, but collapses previous level. I dont know what to do and I'm pretty dissapointed, because it seems like easy thing and I feel like dumb. Someone have any tips please? :(
function MobileCategoriesLinks(props: MobileLinksProps) {
const { data } = useCollectionsQuery();
const { level = 0, onItemClick } = props;
const handleItemClick = (item: any) => {
if (onItemClick) {
onItemClick(item);
}
};
const linksList = data?.collections.items.map((department, index) => {
let item;
if(department.parent?.id === "1") {
const renderItem: RenderItemFn = ({ toggle, setItemRef, setContentRef }) => {
let arrow = null;
let subLinks = null;
let linkOrButton = null;
let link = "/" + department.children
if(department.children?.length) {
arrow = (
<button className="mobile-links__item-toggle" type="button" onClick={toggle}>
<ArrowRoundedDown12x7Svg className="mobile-links__item-arrow" />
</button>
);
}
let depChild = department.children?.map((name) => {
let x = name.name
let link = "/" + name.slug
let child = name
let c = child.children
if(child.children?.length) {
let thirdChild = child.children?.map((item, index) =>{
return item.name
})
let thirdChildSlug = child.children?.map((item, index) =>{
return item.slug
})
arrow = (
<button className="mobile-links__item-toggle" type="button" onClick={toggle}>
<ArrowRoundedDown12x7Svg className="mobile-links__item-arrow" />
</button>
);
subLinks = (
<div className="mobile-links__item-sub-links" ref={setContentRef}>
<ul className="mobile-links mobile--links--level--3">
<li>
<div className="mobile-links__item">
<div className="mobile-links__item-title">
{thirdChild}
</div>
</div>
</li>
</ul>
</div>
)
if(child.children?.length) {
linkOrButton = (
<AppLink
href={"/" + child.slug}
className="mobile-links__item-link"
onClick={() => handleItemClick(link)}
>
{child.name}
</AppLink>
)}
return (
<li>
<div className="mobile-links__item" ref={setItemRef}>
<div className="mobile-links__item-title">
<AppLink
href={"/" + child.slug}
className="mobile-links__item-link"
onClick={() => handleItemClick(link)}
>
{name.name}
</AppLink>
{arrow}
</div>
{subLinks}
</div>
</li>
)}
return (
<li>
<div className="mobile-links__item">
<div className="mobile-links__item-title">
{x}
</div>
</div>
</li>
);
}
)
subLinks = (
<div className="mobile-links__item-sub-links" ref={setContentRef}>
<ul className="mobile-links mobile--links--level--2">
{depChild}
</ul>
</div>
)
if(department.children) {
linkOrButton = (
<AppLink
href={"/" + department.slug}
className="mobile-links__item-link"
onClick={() => handleItemClick(link)}
>
{department.name}
</AppLink>
)} else {
linkOrButton = (
<button
type="button"
className="mobile-links__item-link"
onClick={() => handleItemClick(link)}
>
{department.name}
</button>
)
}
return (
<div className="mobile-links__item" ref={setItemRef}>
<div className="mobile-links__item-title">
{linkOrButton}
{arrow}
</div>
{subLinks}
</div>
);
};
item = <Collapse toggleClass="mobile-links__item--open" render={renderItem} />;
}
return <li key={index}>{item}</li>
}
);
return ( <ul className={`mobile-links mobile-links--level--1`}>
{linksList}
</ul>
);
}
export default withApollo(MobileCategoriesLinks)
Related
I am trying to click on one card of a dynamically created list using map(). I want to click on one card from the array and add a class to it, while at the same time deselecting the other card that was previously clicked. How can I accomplish this? This is what I have so far:
const CardList = () => {
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} {...otherData} />
))}
</div>
);
};
export default CardList;
const Card = ({
headline,
time,
views,
thumbImg,
trainerImg,
workouts,
id
}) => {
const [isSelected, setIsSelected] = useState(false);
const [clickId, setClickId] = useState('');
function handleClick(id) {
setIsSelected(!isSelected);
setClickId(id);
}
return (
<div
className={`card ${isSelected && clickId === id ? 'clicked' : ''}`}
onClick={() => handleClick(id)}
>
<div className='thumbnail-div'>
<img className='thumbnail-img' src={thumbImg} alt='video' />
{workouts ? (
<div className='workout-overlay'>
<p>{workouts}</p>
<p className='workouts'>workouts</p>
</div>
) : null}
</div>
<div className='card-info'>
<div className='card-headline'>
<p>{headline}</p>
<img src={trainerImg} alt='trainer' />
</div>
{time && views ? (
<div className='trainer-data'>
<span>
<i className='glyphicon glyphicon-time'></i>
{time}
</span>
<span>
<i className='glyphicon glyphicon-eye-open'></i>
{views}
</span>
</div>
) : null}
</div>
</div>
);
};
export default Card;
The parent component should control what card is clicked. Add className property to card component:
const Card = ({
//...
className,
onClick
}) => {
//...
return (
<div
className={`card ${className}`}
onClick={() => onClick(id)}
>...</div>
)
}
In parent component pass the className 'clicked' and add the onClick callback to set the selected card:
const CardList = () => {
const [isSelected, setIsSelected] = useState(null);
const handleClick = (id) => {
setIsSelected(id);
}
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} className={isSelected===id && 'clicked'} onClick ={handleClick} {...otherData} />
))}
</div>
);
};
You can do something like this.
First you don't have to set state to each card. Instead Lift state Up.
You define which card is selected in parent so you can pass that to children and add classes if current selected is matching that children.
const CardList = () => {
const [isSelected, setIsSelected] = useState();
const handleCardClick = (id) => {
setIsSelected(id);
}
return (
<div className='card-list'>
{CardData.map(({ id, ...otherData }) => (
<Card key={id} {...otherData} handleClick={handleCardClick} isSelected={isSelected}/>
))}
</div>
);
};
export default CardList;
const Card = ({
headline,
time,
views,
thumbImg,
trainerImg,
workouts,
id,
isSelected,
handleClick
}) => {
return (
<div
className={`card ${isSelected === id ? 'clicked' : ''}`}
onClick={() => handleClick(id)}
>
<div className='thumbnail-div'>
<img className='thumbnail-img' src={thumbImg} alt='video' />
{workouts ? (
<div className='workout-overlay'>
<p>{workouts}</p>
<p className='workouts'>workouts</p>
</div>
) : null}
</div>
<div className='card-info'>
<div className='card-headline'>
<p>{headline}</p>
<img src={trainerImg} alt='trainer' />
</div>
{time && views ? (
<div className='trainer-data'>
<span>
<i className='glyphicon glyphicon-time'></i>
{time}
</span>
<span>
<i className='glyphicon glyphicon-eye-open'></i>
{views}
</span>
</div>
) : null}
</div>
</div>
);
};
export default Card;
I have a component with several elements. I'm trying to figure out how to update the code with hooks so that only one element will be open at a time - when a element is open, the other's should be closed. This is the code:
const HowItWorks = ({ content, libraries }) => {
const Html2React = libraries.html2react.Component;
return (
<HowItWorksContainer>
{content.fields.map((tab, i) => {
const [open, setOpen] = useState(false);
const onToggle = () => {
setOpen(!open);
};
return (
<details
key={i}
onToggle={onToggle}
className={`tab ${open ? "open" : "closed"}`}
>
<summary className="tab__heading">
<div className="wrapper">
<p>{tab.heading}</p>
{open ? (
<i className="icon kap-arrow-minus" />
) : (
<i className="icon kap-arrow-plus" />
)}
</div>
</summary>
<div className="tab__content">
<Html2React html={tab.content} />
</div>
</details>
);
})}
</HowItWorksContainer>
);
};
Instead of having the open state be a boolean, make it be the ID of the element that is open. Then you can have a function that returns if the element is open by comparing the state with the ID.
const HowItWorks = ({ content, libraries }) => {
const [open, setOpen] = useState(0); //Use the element ID to check which one is open
const onToggle = (id) => {
setOpen(id);
};
const isOpen = (id) => {
return id === open ? "open" : "closed";
}
const Html2React = libraries.html2react.Component;
return (
<HowItWorksContainer>
{content.fields.map((tab, i) => {
return (
<details
key={i}
onToggle={onToggle}
className={`tab ${isOpen(i)}`}
>
<summary className="tab__heading">
<div className="wrapper">
<p>{tab.heading}</p>
{!!isOpen(i) ? (
<i className="icon kap-arrow-minus" />
) : (
<i className="icon kap-arrow-plus" />
)}
</div>
</summary>
<div className="tab__content">
<Html2React html={tab.content} />
</div>
</details>
);
})}
</HowItWorksContainer>
);
};
I'm doing pokedex (pokemon wiki stuff). I want to change my component view, when clicking on pokemon images (description lookalike). When I click on an image - nothing happens (firstly, I want at least pokemon's name to be added to the pokemonDescription array). What am I doing wrong?
let pokemonDescription = [];
const useForceUpdate = () => {
const [value, setValue] = useState(true);
return () => setValue(value => !value);
}
const forceUpdate = useForceUpdate();
const onPokemonClick = (event) => {
console.log(
"wrapper clicked, event.target - ",
event.target.getAttribute('data-name')
);
pokemonDescription = [];
pokemonDescription.push(event.target.getAttribute('data-name'));
console.log("description array -", pokemonDescription);
forceUpdate();
};
useEffect(() => {
document.querySelector(".wrapper").addEventListener("click", onPokemonClick);
...
return () => {
document.querySelector(".wrapper").removeEventListener("click", onPokemonClick);
};
}, []);
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
data-name={p.name}
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{ pokemonDescription }
</div>
</div>
)
You should add pokemonDescription to your component state
const [pokemonDescription, setPokemonDescription] = useState([]);
Remove the forceUpdate function and hook, it is unnecessary.
Attach the click handlers to the elements with the data-name attribute you are trying to handle.
Map the pokemonDescription state array to renderable JSX. I simply used a div, but you should use whatever your UI design requires.
const onPokemonClick = (event) => {
setPokemonDescription(names => [
...names,
event.target.getAttribute('data-name'),
]);
};
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
data-name={p.name}
onClick={onPokemonClick} // <-- attach click handler to img element
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{pokemonDescription.map(name => (
<div>{name}</div>
))}
</div>
</div>
)
Add pokemonDescription to state instead of some local variable and it will solve your issue.
Try to avoid using forceUpdate, most of the times it means only that you are doing something silly.
I don't what that useForceUpdate does , but here is how would go about adding pokemon names to description array which is a state variable in my answer
const [pokemonDescription , setPokemonDescription ] = useState(null);
const onPokemonClick = (p) => {
const tempPokemonDescription = [...pokemonDescription ];
pokemonDescription.push(p.name);
console.log("description array -", pokemonDescription);
setPokemonDescription(tempPokemonDescription )
};
...
return (
<div className="Pokemons">
<div className="column pokemons-list">
<div className="wrapper">
{
pokemonsData.map((p, id) => (
<div className="box" onClick={e=>onPokemonClick(p)} key={ id }>
<img
src={ p.sprites.front_default }
alt="pokemon-img"
title={ p.name }
className="icon"
/>
{ p.name}
<div className="container">
{ pokemonsTypes[id] }
</div>
</div>
))
}
</div>
...
</div>
<div className="column description">
{ pokemonDescription }
</div>
</div>
)
I'm having some issues with child re-rendering, I pass methods to children to see if a button should be displayed or not but when the state of the parent changes, the children are not re-rendered.
I tried with the disabled attribute for the button but didn't work either.
Here's my code (I removed unnecessary part):
function Cards(props) {
const isCardInDeck = (translationKey) => {
return props.deck.some(
(card) => !!card && card.translationKey === translationKey
);
};
const addToDeck = (card) => {
if (!isCardInDeck(card.translationKey) && !!card) {
props.deck.push(card);
}
};
const removeFromDeck = (card) => {
if (isCardInDeck(card.translationKey) && !!card) {
var index = props.deck.findIndex(
(c) => c.translationKey === card.translationKey
);
props.deck.splice(index, 1);
}
};
return (
<div className="cardsContent">
<div className="cards">
{cardList.length > 0 ? (
cardList.map((item, index) => {
return (
<Card key={index} card={item} addToDeckDisabled={isCardInDeck(item.translationKey)} addToDeckClick={addToDeck} removeFromDeckClick={removeFromDeck} />
);
})
) : (
<span>
<FormattedMessage id="app.cards.label.no.card.found" defaultMessage="No card found with filter."/>
</span>
)}
</div>
</div>
);
}
function Card(props) {
const toggleShowDescription = () => {
if (!showDescription) {
setShowDescription(!showDescription);
}
};
return (
<div onClick={toggleShowDescription} onBlur={toggleShowDescription} >
<img src={"../images/cards/" + props.card.image} alt={props.card.image + " not found"} />
{showDescription ? (
<div className="customCardDetail">
<div className="cardName"></div>
<div className="cardType">
{props.addToDeckDisabled ? (
<Button onClick={() => { props.removeFromDeckClick(props.card);}} startIcon={<RemoveIcon />}>
Remove from deck
</Button>
) : (
<Button onClick={() => { props.addToDeckClick(props.card); }} startIcon={<AddIcon />}>
Add to deck
</Button>
)}
</div>
<div className="cardDescription">
<span>
<FormattedMessage id={props.card.description} defaultMessage={props.card.description} />
</span>
</div>
</div>
) : (
""
)}
</div>
);
}
You code does not update state. Cards mutates the props that it is receiving.
To use state in a functional component in React you should use the useState hook.
Cards would then look something like this:
function Cards(props) {
const [deck, setDeck] = useState(props.initialDeck)
const isCardInDeck = (translationKey) => {
return deck.some(
(card) => !!card && card.translationKey === translationKey
);
};
const addToDeck = (card) => {
if (!isCardInDeck(card.translationKey) && !!card) {
setDeck([...deck, card])
}
};
const removeFromDeck = (card) => {
if (isCardInDeck(card.translationKey) && !!card) {
setDeck(deck.filter(deckItem => deckItem.translationKey !== card.translationKey))
}
};
return (
<div className="cardsContent">
<div className="cards">
{cardList.length > 0 ? (
cardList.map((item, index) => {
return (
<Card key={index} card={item} addToDeckDisabled={isCardInDeck(item.translationKey)} addToDeckClick={addToDeck} removeFromDeckClick={removeFromDeck} />
);
})
) : (
<span>
<FormattedMessage id="app.cards.label.no.card.found" defaultMessage="No card found with filter."/>
</span>
)}
</div>
</div>
);
}
In the following React component:
import React, { useState, useEffect } from 'react';
const List = () => {
const docLimit = 2;
const maxColNumber = 6;
const [cards, setCards] = useState([]);
const [beginAfter, setBeginAfter] = useState(0);
return (
<>
<div className='collection'>
<div className='collection__set' onClick={() => setBeginAfter(0)}>
First 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 1)}
>
Next 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 2)}
>
Next 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 3)}
>
Next 2 data
</div>
</div>
<ul className='list'>
{cards.map((card) => (
<li key={card.id} className='list__item'>
<DeleteCard card={card} />
</li>
))}
</ul>
</>
);
};
export default List;
I want to use a loop to output this part:
<div className='collection'>
<div className='collection__set' onClick={() => setBeginAfter(0)}>
First 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 1)}
>
Next 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 2)}
>
Next 2 data
</div>
<div
className='collection__set'
onClick={() => setBeginAfter(docLimit * 3)}
>
Next 2 data
</div>
</div>
But it doesn't matter what I try, it doesn't work.
I tried this:
const renderMenu = () => {
const menu = [];
for (var i = 0; i < maxColNumber; i++) {
menu.push(
<div
className='collection__set'
onClick={setBeginAfter(docLimit * i)}
>
Next 2 data
</div>
);
}
return (
<div className='collection'>menu</div>
)
};
But it didn't work.
Can someone please help me with this?
Insted of using for, you can use map.
Try this method:
const Menu = () => Array(maxColNumber / 2).fill().map((_, i) => {
const onClick = () => setBeginAfter(docLimit * i)
return (
<div className='collection'>
<div
className='collection__set'
onClick={onClick}
>
Next 2 data
</div>
</div>
)
})
// later in your code , just add the tag
<Menu />