I want to create something similar to an accordeon:
https://codesandbox.io/s/suspicious-golick-xrvt5?file=/src/App.js
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const items = [
{
title: "lorem1",
content: "content1"
},
{
title: "lorem2",
content: "content2"
}
];
const [hide, setHide] = useState({
title: "",
open: false
});
const hideContent = (title) => {
setHide({
title: title,
open: !hide.open
});
};
return (
<div className="App">
{items.map((i) => {
return (
<div>
<h1 onClick={() => hideContent(i.title)}>{i.title}</h1>
{hide.title === i.title && hide.open ? <p>{i.content}</p> : ""}
</div>
);
})}
</div>
);
}
Now when i click on a title its content appears, but when click on another title the first content disappear but the actual clicked does not appear. How to click on the second title and to hide the previous content and in the same time to open the actual clicked item content?
I think instead of going with open state. Go with id. You item state
const items = [
{
id: 0,
title: "lorem1",
content: "content1"
},
{
id: 1,
title: "lorem2",
content: "content2"
}
];
Pass id in hideContent:
const hideContent = (title, id) => {
setHide(
(prev) =>
({
title: title,
open: prev.open === id ? null : id
})
);
}
and condition to check inside return like this:
hide.title === i.title && hide.open === i.id ? (
<p>{i.content}</p>
) : (
""
)}
here is demo and full code: https://codesandbox.io/s/upbeat-brattain-8yfx7?file=/src/App.js
Related
How do I remove duplicate code from titles.map and titles.slice?
how should be handled with en effect to set the facets that should be displayed. Functionality works as expected, I just want to remove duplicated code.
import { useState } from "react";
const App = () => {
const titles = [
{ id: "0", name: "Title" },
{ id: "5637144579", name: "Miss" },
{ id: "5637144576", name: "Mr." },
{ id: "5637145326", name: "MrandMrs." },
{ id: "5637144577", name: "Mrs." },
{ id: "5637144578", name: "Ms." },
{ id: "5637145330", name: "Br." },
{ id: "5637145327", name: "Dame" },
{ id: "5637144585", name: "Dr." },
{ id: "5637145331", name: "Fr." },
{ id: "5637144582", name: "I" },
];
const [isAllFacets, setIsAllFacets] = useState(false);
const MAX_FACET_COUNT = 5;
const visibleFacetCount = titles.length - 1 === MAX_FACET_COUNT ?
titles.length :
MAX_FACET_COUNT;
const showAllFacet = () => { setIsAllFacets(!isAllFacets); };
return (<>
{isAllFacets ?
titles.map((title: any) => {
return <div key={title.id}>{title.name}</div>;
}) :
titles.slice(0, visibleFacetCount).map((title) => {
return <div key={title.id}>{title.name}</div>;
})}
{titles.length > visibleFacetCount && (<>
{!isAllFacets ? (
<button onClick={showAllFacet}>show all</button>
) : (
<button onClick={showAllFacet}>show less</button>
)}
</>)}
</>);
};
export default App;
One option would be to use the conditional operator to slice the entire existing array in case isAllFacets is false - instead of alternating over the whole JSX to return, alternate over only the index to slice.
A similar approach can be used to simplify your <button> text.
return (
<>
{
titles
.slice(0, isAllFacets ? titles.length : visibleFacetCount)
.map(title => <div key={title.id}>{title.name}</div>)
}
{titles.length > visibleFacetCount && <button onClick={showAllFacet}>show{isAllFacets ? ' all' : ' less'}</button>}
</>
);
You can achieve your goal with useMemo hook. Prepare the data to display and render it. Value will be recalculated when anything inside depsArray is changed.
const titlesToDisplay = useMemo(() => {
return isAllFacets ? titles : titles.slice(0, visibleFacetCount);
}, [titles, isAllFacets, visibleFacetCount]);
return (
<>
{titlesToDisplay.map((title) => {
return <div key={title.id}>{title.name}</div>;
})}
{titles.length > visibleFacetCount && (
<>
{!isAllFacets ? (
<button onClick={showAllFacet}>show all</button>
) : (
<button onClick={showAllFacet}>show less</button>
)}
</>
)}
</>
);
you only need to put all together :
titles.slice(0, isAllFacets?titles.length:visibleFacetCount).map((title) => { return <div key={title.id}>{title.name}
</div>; })
Create a renderTitle function which returns the title div
const renderTitle = title => <div key={title.id}>{title.name}</div>
Then pass it to both .map invocations
titles.map(renderTitle)
and
titles.slice(0, visibleFacetCount).map(renderTitle)
And for your button, simplify it with:
<button onClick={showAllFacet}>show {isAllFacets ? "less" : "all"}</button>
I am trying to create a simple button multi-select in React but I'm currently getting unexpected behaviour. I'd like users to be able to toggle multiple buttons and have them colourise accordingly, however the buttons seem to act a bit randomly.
I have the following class
export default function App() {
const [value, setValue] = useState([]);
const [listButtons, setListButtons] = useState([]);
const BUTTONS = [
{ id: 123, title: 'button1' },
{ id: 456, title: 'button2' },
{ id: 789, title: 'button3' },
];
const handleButton = (button) => {
if (value.includes(button)) {
setValue(value.filter((el) => el !== button));
} else {
let tmp = value;
tmp.push(button);
setValue(tmp);
}
console.log(value);
};
const buttonList = () => {
setListButtons(
BUTTONS.map((bt) => (
<button
key={bt.id}
onClick={() => handleButton(bt.id)}
className={value.includes(bt.id) ? 'buttonPressed' : 'button'}
>
{bt.title}
</button>
))
);
};
useEffect(() => {
buttonList();
}, [value]);
return (
<div>
<h1>Hello StackBlitz!</h1>
<div>{listButtons}</div>
</div>
);
}
If you select all 3 buttons then select 1 more button the css will change.
I am trying to use these as buttons as toggle switches.
I have an example running #
Stackblitz
Any help is much appreciated.
Thanks
I think that what you want to achieve is way simpler:
You just need to store the current ID of the selected button.
Never store an array of JSX elements inside a state. It is not how react works. Decouple, only store the info. React component is always a consequence of a pattern / data, never a source.
You only need to store the necessary information, aka the button id.
Information that doesn't belong to the state of the component should be moved outside. In this case, BUTTONS shouldn't be inside your <App>.
Working code:
import React, { useState } from 'react';
import './style.css';
const BUTTONS = [
{ id: 123, title: 'button1', selected: false },
{ id: 456, title: 'button2', selected: false },
{ id: 789, title: 'button3', selected: false },
];
export default function App() {
const [buttons, setButtons] = useState(BUTTONS);
const handleButton = (buttonId) => {
const newButtons = buttons.map((btn) => {
if (btn.id !== buttonId) return btn;
btn.selected = !btn.selected;
return btn;
});
setButtons(newButtons);
};
return (
<div>
<h1>Hello StackBlitz!</h1>
<div>
{buttons.map((bt) => (
<button
key={bt.id}
onClick={() => handleButton(bt.id)}
className={bt.selected ? 'buttonPressed' : 'button'}
>
{bt.title}
</button>
))}
</div>
</div>
);
}
I hope it helps.
Edit: the BUTTONS array was modified to add a selected property. Now several buttons can be selected at the same time.
I have been persistently working on this problem where the goal is to drag a card form 'Column 1' and copy that into another column say 'Column 2'.
Now when my first card is dragged and drop it into 'Column 2, the card is accordingly added to that column, but when I drag another card and drop into 'Column 2' instead of being appended it just replaces the existing card with itself.
I have been debugging the state, but the issue still persists. I haven't gotten a clue what am I doing wrong here?
Here's my code
// Card Component
function Card({ id, text, isDrag }) {
const [, drag] = useDrag(() => ({
type: "bp-card",
item: () => {
return { id, text}
},
collect: monitor => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: () => isDrag
}));
return (
<div
className='card'
ref={drag}
style={{
cursor: isDrag ? 'pointer' : 'no-drop'
}}
>
{text}
</div>
)
}
// Column Component
function Column({ title, children, onCardDropped }) {
const [, drop] = useDrop(() => ({
accept: "bp-card",
drop: item => {
onCardDropped(item);
}
}));
return (
<div className="flex-item" ref={title === 'Column 2' ? drop : null}>
<p>{title}</p>
{children.length > 0 && children.map(({ id, text, isDrag }) => (
<Card
key={id}
id={id}
text={text}
isDrag={isDrag}
/>
))}
</div>
)
}
// Main App
function App() {
const [cards] = useState([
{ id: 1, text: 'Card 1', isDrag: true },
{ id: 2, text: 'Card 2', isDrag: true },
]);
const [columns, setColumns] = useState([
{
id: 1,
title: 'Column 1',
children: cards
},
{
id: 2,
title: 'Column 2',
children: []
},
]);
const onCardDropped = ({ id, text }) => {
// let card = null;
const targetColumnId = 2;
const transformedColumns = columns.map(column => {
if (column.id === targetColumnId) {
return {
...column,
children: [
...column.children,
{ id, text }
]
}
}
return column;
});
setColumns(transformedColumns);
}
return (
<DndProvider backend={HTML5Backend}>
<div className='flex-container'>
{columns.map((column) => (
<Column
key={column.id}
title={column.title}
children={column.children}
onCardDropped={onCardDropped}
/>
))}
</div>
</DndProvider>
);
}
Any help is highly appreciated. Thanks.
You need to consider the previous state using the callback of the set state method. It starts to work after changing the onCardDropped as below.
const onCardDropped = ({ id, text }) => {
// let card = null;
const targetColumnId = 2;
setColumns((prevColumns) =>
prevColumns.map((column) => {
if (column.id === targetColumnId) {
return {
...column,
children: [...column.children, { id, text }]
};
}
return column;
})
);
};
It's always a good idea to use the state from the callback method as opposed to using the state object directly which might be stale.
Working Demo
im making a portfolio website and have multiple different buttons with skills which contain an img and p tag. I want to show the description of each tag everytime a user clicks on the button. how can I do this? right now everytime user clicks it, all buttons show description.
const Skills = () => {
const [state, setState] = useState(false)
let skills = [
{ id: 1, desc: 'HTML5', state: false, img: htmlIcon },
{ id: 2, desc: 'CSS3', state: false, img: cssIcon },
{ etc....}
const showDesc = (id) => {
console.log(skills[id-1] = !state);
setState(!state)
}
return (
{skills.map(skill => (
<button onClick={(id) => showDesc(skill.id)}>
<img style={ state ? {display:'none'} : {display:'block'}} src={skill.img} />
<p style={ state ? {display:'block'} : {display:'none'}}>{skill.desc}</p>
</button>
))}
I recommend to manipulate element state instead of entry list. But if you really need to manipulate entry list you should add that list to your state. Then when you want to show/hide specific item, you need to find that item in state and correctly update entry list by making a copy of that list (with updated item). For example you can do it like this:
import React, { useState } from 'react';
const Skills = () => {
const [skills, setSkills] = useState([
{
id: 1,
desc: 'HTML5',
state: false,
img: htmlIcon, // your icon
},
{
id: 2,
desc: 'CSS3',
state: false,
img: cssIcon, // your icon
},
]);
const showDesc = (id) => {
const newSkills = skills.map((item) => {
if (item.id === id) {
return {
...item,
state: !item.state,
};
}
return item;
});
setSkills(newSkills);
};
return (
<div>
{skills.map(({
id,
img,
state,
desc,
}) => (
<button type="button" key={id} onClick={() => showDesc(id)}>
<img alt="img" style={state ? { display: 'none' } : { display: 'block' }} src={img} />
<p style={state ? { display: 'block' } : { display: 'none' }}>{desc}</p>
</button>
))}
</div>
);
};
Instead of manipulating all list, you can try to move show/hide visibility to list item itself. Create separate component for item and separate component for rendering that items. It will help you to simplify logic and make individual component responsible for it visibility.
About list rendering you can read more here
For example you can try something like this as alternative:
import React, { useState } from 'react';
const skills = [
{
id: 1,
desc: 'HTML5',
img: htmlIcon, // your icon
},
{
id: 2,
desc: 'CSS3',
img: cssIcon, // your icon
},
];
const SkillItem = ({
img,
desc = '',
}) => {
const [visibility, setVisibility] = useState(false);
const toggleVisibility = () => {
setVisibility(!visibility);
};
const content = visibility
? <p>{desc}</p>
: <img alt="img" src={img} />;
return (
<div>
<button type="button" onClick={toggleVisibility}>
{content}
</button>
</div>
);
};
const SkillList = () => skills.map(({
id,
img,
desc,
}) => <SkillItem img={img} desc={desc} key={id} />);
I am trying to implement toggle functionality, by using this functionality user can select desired single preference, and also the user can select all preferences by using the "Select All" button. I have implemented the code that is supporting a single selection I want to make select all functionality.
This is how i am handling toggle
const toggleItem = useCallback((isToggled, value) => {
if (isToggled) {
setToggledItems((prevState) => [...prevState, value]);
} else {
setToggledItems((prevState) => [...prevState.filter((item) => item !== value)]);
}
}, []);
const [toggledItems, setToggledItems] = useState([]);
var eventsnfo = [
{
icon: '',
title: 'select All',
subTitle: '',
},
{
icon: 'event1',
title: 'event1',
subTitle: 'event1desc',
},
{
icon: 'event2',
title: 'event2',
subTitle: 'event2desc',
},
{
icon: 'event3',
title: 'event3',
subTitle: 'event3desc',
},
{
icon: 'event4',
title: 'event4',
subTitle: 'event4desc',
},
];
this is how i am loading all toggle sections
<div>
{eventsnfo?.map(({ icon, title, subTitle }, index) => {
return (
<>
<div key={index} className='events__firstSection'>
<div className='events__agreeToAllContainer'>
{icon && (
<Icon name={icon} className='events__noticeIcon' isForceDarkMode />
)}
<div className={icon ? 'events__text' : 'events__text events__leftAlign '}>
{title}
</div>
</div>
<Toggle
containerClass='events__toggle'
checked={toggledItems.includes(title)}
onToggle={(isToggled) => toggleItem(isToggled, title)}
/>
</div>
{subTitle && <div className='events__description'>{subTitle}</div>}
<div className={index !== eventsnfo.length - 1 && 'events__divider'}></div>
</>
);
})}
</div>;
I think you can toggle all by changing your toggleItem function
const toggleItem = (isToggled, value) => {
let items = [...toggledItems];
if (isToggled) {
items =
value === "select All"
? eventsnfo?.map((events) => events.title)
: [...items, value];
if (items?.length === eventsnfo?.length - 1) {
items.push("select All");
}
} else {
items =
value === "select All"
? []
: [...items.filter((item) => item !== value && item !== "select All")];
}
setToggledItems(items);
};
Working Demo