I have an array of job descriptions that I want to hide a part of each description and show it completely when a button is clicked using React hooks.
I am iterating over the array( consists of id and description) to show all the descriptions as a list in the component. There is a button right after each paragraph to show or hide the content.
readMore is used to hide/show the content and
activeIndex is used to keep track of clicked item index.
This is what I have done so far:
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [readMore, setReadMore] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{readMore ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
setActiveIndex(index);
if (activeIndex === id) {
setReadMore(!readMore);
}
}}
>
{readMore ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
The problem is that when I click one button it toggles all the items in the list.
I want to show/hide content only when its own button clicked.
Can somebody tell me what I am doing wrong?
Thanks in advance.
Your readMore state is entirely redundant and is actually causing the issue. If you know the activeIndex, then you have all the info you need about what to show and not show!
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [activeIndex, setActiveIndex] = useState(null);
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{activeIndex === index ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
if (activeIndex) {
setActiveIndex(null);
} else {
setActiveIndex(index);
}
}}
>
{activeIndex === index ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
Edit: The aforementioned solution only lets you open one item at a time. If you need multiple items, you need to maintain an accounting of all the indices that are active. I think a Set would be a perfect structure for this:
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [activeIndices, setActiveIndices] = useState(new Set());
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{activeIndices.has(index) ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
const newIndices = new Set(activeIndices);
if (activeIndices.has(index)) {
newIndices.delete(index);
} else {
newIndices.add(index);
}
setActiveIndices(newIndices);
}}
>
{activeIndices.has(index) ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
Try this
{readMore && (activeIndex === id) ? description : `${description.substring(0, 250)}...`}
function Destination() {
const travels = [
{
title: "Home"
},
{
title: "Traveltype",
subItems: ["Local", "National", "International"]
},
{
title: "Contact",
subItems: ["Phone", "Mail", "Chat"]
}
];
const [activeIndex, setActiveIndex] = useState(null);
return (
<div className="menu-wrapper">
{travels.map((item, index) => {
return (
<div key={`${item.title}`}>
{item.title}
{item.subItems && (
<button
onClick={() => {
if (activeIndex) {
if (activeIndex !== index) {
setActiveIndex(index);
} else {
setActiveIndex(null);
}
} else {
setActiveIndex(index);
}
}}
>
{activeIndex === index ? `Hide` : `Expand`}
</button>
)}
{activeIndex === index && (
<ul>
{item.subItems &&
item.subItems.map((subItem) => {
return (
<li
key={`li-${item.title}-${subItem}`}
>
{subItem}
</li>
);
})}
</ul>
)}
</div>
);
})}
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Related
I have cards with categories, clicking on which opens hidden content. I need the card to close when switching between them and if a click occurs behind the content.
import React, { useRef } from "react";
import s from "./Shop.module.css";
import { useState } from "react";
export const Shop = () => {
const card = [
{
name: "Brands",
cat: ["Adidas", "Nike", "Reebok", "Puma", "Vans", "New Balance"],
show: false,
id: 0,
},
{
name: "Size",
cat: ["43", "43,5"],
show: false,
id: 1,
},
{
name: "Type",
cat: ["Sneakers ", "Slippers"],
show: false,
id: 2,
},
];
const [active, setActive] = useState({});
const handleActive = (id) => {
setActive({ ...active, [id]: !active[id] });
};
const handleDisable = (index) => {
setActive(index);
};
return (
<div className={s.container}>
<div className={s.brandInner}>
{card.map((i, index) => {
return (
<div className={s.brandCard} key={i.id}>
<button
className={`${s.brandBtn} `}
onClick={() => handleActive(i.id, index)}
onBlur={() => handleDisable(index)}
>
<p className={`${active[i.id] ? `${s.brandBtnActive}` : ``}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : `${s.dNone}`}`}
>
<ul className={s.brandList}>
{i.cat.map((elem) => {
return (
<li key={elem} className={s.brandItem}>
{elem}
</li>
);
})}
</ul>
<button className={s.brandOpenBtn}>Apply</button>
</div>
</div>
);
})}
</div>
</div>
);
};
I tried to do it through onBlur, but this way I can't interact with the content that appears when opening the card, please help
You could do this a few different ways, here are two.
Array version
You can do this by using a array to keep track of the ids that are active.
const [active, setActive] = useState([]);
For the event handler we will creata a new toggleActive function which replaces the others. This will check if the id is already in the array and if so remove it, else add it.
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive.includes(id)) {
return prevActive.filter((activeId) => activeId !== id);
}
return [...prevActive, id];
});
};
Then in the return of the component we need to updated some logic as well. Then handlers only take in the id of the i. To check if the id is in the array with can use includes.
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active.includes(i.id) ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active.includes(i.id) ? "" : s.dNone}`}
>
Object version
This version is to do it with a object.
const [active, setActive] = useState({});
The handler, this will toggle the value of the id starting with false if there is no value for the id yet.
const toggleActive = (id) => {
setActive((prevActive) => {
const prevValue = prevActive[id] ?? false;
return {
...prevActive,
[id]: !prevValue,
};
});
};
The elements
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active[i.id] ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : s.dNone}`}
>
Edit: toggle with closing others
First we declare the state with a initial value of null
const [active, setActive] = useState(null)
We create the toggleActive function which checks if the id to toggle is the previous id, if so return null else return the new active id
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive === id) return null;
return id;
});
};
For the rendering it is quite simple, add the toggleActive function to the button and check if the active is the same id
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active === i.id ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active === i.id ? "" : s.dNone}`}
>
I have a component A, which displays contents of a component B conditionally. Component B is contains a list of items, and when one clicks one of the items in the list, a new layout is supposed to be fired up showing details of the item. When i try to pass the props to switch to a new layout on a component B list item, i get an error toggleSearchType is not a function . Any assistance or recommendation on what i might be doing wrong will be appreciated.
My index file looks like this :
const PatientSearch: React.FC<PatientSearchProps> = ({ closePanel }) => {
const { t } = useTranslation();
const [searchType, setSearchType] = useState<SearchTypes>(SearchTypes.BASIC);
const toggleSearchType = (searchType: SearchTypes) => {
setSearchType(searchType);
};
return (
<>
<Overlay header={t('addPatientToList', 'Add patient to list')} closePanel={closePanel}>
<div className="omrs-main-content">
{searchType === SearchTypes.BASIC ? (
<BasicSearch toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.ADVANCED ? (
<PatientScheduledVisits toggleSearchType={toggleSearchType} />
) : searchType === SearchTypes.SCHEDULED_VISITS ? (
<AdvancedSearch toggleSearchType={toggleSearchType} />
) : null}
</div>
</Overlay>
</>
);
};
The searchtypes are as below :
export enum SearchTypes {
BASIC = 'basic',
ADVANCED = 'advanced',
SCHEDULED_VISITS = 'scheduled-visits'
}
My component A looks like this :
import React, { useEffect, useMemo, useState } from 'react';
interface BasicSearchProps {
toggleSearchType: (searchMode: SearchTypes) => void;
}
const BasicSearch: React.FC<BasicSearchProps> = ({ toggleSearchType }) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<any>(null);
const customRepresentation = '';
return (
<div className={searchResults?.length ? styles.lightBackground : styles.resultsContainer}>
{searchResults?.length ? (
<div className={styles.resultsContainer}>{<SearchResults toggleSearchType={searchResults} patients={searchResults} />}</div>
) : (
<div>
<div className={styles['text-divider']}>{t('or', 'Or')}</div>
<div className={styles.buttonContainer}>
<Button
kind="ghost"
iconDescription="Advanced search"
renderIcon={Search16}
onClick={() => toggleSearchType(SearchTypes.ADVANCED)}>
{t('advancedSearch', 'Advanced search')}
</Button>
</div>
</div>
)}
</div>
);
};
export default BasicSearch;
Component B looks like this :
interface SearchResultsProps {
patients: Array<any>;
hidePanel?: any;
toggleSearchType: (searchMode: SearchTypes) => void;
}
function SearchResults({ patients, toggleSearchType }: SearchResultsProps ) {
const fhirPatients = useMemo(() => {
return patients.map((patient) => {
const preferredAddress = patient.person.addresses?.find((address) => address.preferred);
});
}, [patients]);
return (
<>
{fhirPatients.map((patient) => (
<div key={patient.id} className={styles.patientChart} onClick={() => toggleSearchType(SearchTypes.SCHEDULED_VISITS)} >
<div className={styles.container}>
<ExtensionSlot
extensionSlotName="patient-header-slot"
state={{
patient,
patientUuid: patient.id,
// onClick: onClickSearchResult,
}}
/>
</div>
</div>
))}
</>
);
}
}
This is probably really easy, I want to show the info of a list item when a user clicks on the item, however, the code I have will open the info of all list items when clicked.
https://codesandbox.io/s/friendly-lichterman-j1vpd?file=/src/App.js:0-599
import React, { useState } from "react";
import "./styles.css";
const list = [1, 2, 3];
export default function App() {
const [active, setActive] = useState(false);
return (
<div>
{list.map((item, idx) => {
return (
<>
<li
onClick={() => {
setActive(!active);
}}
>
{item}
<div className={active ? "active" : "info"}>
{" "}
Info {idx + 1}
</div>
</li>
</>
);
})}
</div>
);
}
You are trying to toggle active only which is used by all items. Instead use index position to toggle.
I have explained the rest within the code using comments.
App.js
import React, { useState } from "react";
import "./styles.css";
const list = [1, 2, 3];
export default function App() {
const [active, setActive] = useState();
return (
<div>
{/* You forgot to add ul here */}
<ul>
{list.map((item, idx) => {
return (
// Added key to each child to avoid error. Use <React.Fragment/> instead of <>
<React.Fragment key={idx}>
<li
onClick={() => {
// Condition for toggling the lists.
// If current list is selected
if (active === idx) {
// change active to blank
setActive();
} else {
// change active to current index
setActive(idx);
}
}}
>
{item}
</li>
<div className={active === idx ? "info active" : "info"}>
{" "}
Info {idx + 1}
</div>
</React.Fragment>
);
})}
</ul>
</div>
);
}
Edited this css to avoid applying to another tag
style.css
.info.active {
display: flex;
}
You can try the above code on sandbox
https://codesandbox.io/s/stackoverflow-qno-65730790-tmeyf
Seems like a good use case for useReducer. It's a really useful tool to keep track of the states of multiple components. In your case you would need to track whether a given LI is active (showing information) or not. Here is how to do that with useReducer along with a Sanbdox
import React, { useState } from "react";
import "./styles.css";
const list = [1, 2, 3];
const default_states = list.map((item) => Object({ id: item, action: false }));
export default function App() {
const [li_states, dispatch] = React.useReducer((state, id) => {
return state.map((item) => {
if (item.id === id) return { id: item.id, active: !item.active };
else return item;
});
}, default_states);
return (
<div>
{list.map((item) => {
const cur = li_states.find((s) => s.id === item);
return (
<div key={item}>
<li
onClick={() => {
dispatch(item);
}}
>
{item}
</li>
<div className={cur.active ? "action" : "info"}> Info {item}</div>
</div>
);
})}
</div>
);
}
What's happening here is each time you click any of your LIs, the dispatch calls the reducer function inside React.useReducer with the ID of the LI and toggles the state of the clicked LI.
You can follow this method too
const list = ['Start', 'Installation', 'Text Banners', 'Image Banners'];
const links = ['/start', '/installation', '/textbanners', '/imagebanners'];
const [active, setActive] = useState(null)
const toggleActive = (e) => {
console.log(e)
setActive(e.target.innerText)
}
return (
<div className={style.dashboard_container}>
<div className={style.dashboard}>
<ul>
{list.map((item, index) => {
return (
<li className={active == item ? style.active : ''} key={index} onClick={toggleActive}>{item}</li>
)
})}
</ul>
</div>
{children}
</div>
);
Hello I have a single list component which render some <Card> components that has a prop isSelected . Because alot of things happens when a <Card> Component has isSelected === true I added the state on the <List> component, and I want when someone clicks a card to check:
1) If there are no previously selected items ( state===null to add that item's id to state )
2) If someone clicks the same item or another item while there is already an item selected in state, to just unselected the active item.
import {Card} from "./Card";
import cloneDeep from 'lodash/cloneDeep';
const List = () => {
const [selectedCard, setSelectedCard] = useState(null);
const onCardClick = id => {
console.debug(selectedCard, id)
const newSelectedCard = cloneDeep(selectedCard);
// if he clicks another item while an item is active
// or if he clicks the same item while active
// should just make it inactive
if (newSelectedCard !== null || newSelectedCard === id) {
setSelectedCard(null)
} else {
setSelectedCard(id)
}
console.debug(selectedCard, id)
}
return (
<ul className="card-list">
{cardData.map(card => (
<Card
onClick={() => onCardClick(card.id)}
key={card.id}
isSelected={selectedCard === card.id}
{...card}
/>
))}
</ul>
)
}
export const CardList = () => (
<List/>
);
The issue is that the 2 console.debugs print the same values which means that the state doesnt update imediately and Im experiencing some strange behaviours here and there. Am I missing something here?
Basically you need to follow 3 condition as below
if(newSelectedCard === null){
setSelectedCard(id)
}
else if(newSelectedCard === id){
setSelectedCard(null);
}
else{
setSelectedCard(id)
}
Here is the Complete example:
import cloneDeep from 'lodash/cloneDeep';
import React, {useState} from "react";
const List = () => {
const [cardData, setCardData] = useState([
{id: 1, title: 'First Card'},
{id: 2, title: 'Second Card'},
{id: 3, title: 'Third Card'},
{id: 4, title: 'Fourth Card'},
]);
const [selectedCard, setSelectedCard] = useState(null);
const onCardClick = id => {
console.log(selectedCard, id);
const newSelectedCard = cloneDeep(selectedCard);
// if he clicks another item while an item is active
// or if he clicks the same item while active
// should just make it inactive
if(newSelectedCard === null){
setSelectedCard(id)
}
else if(newSelectedCard === id){
setSelectedCard(null);
}
else{
setSelectedCard(id)
}
console.log(selectedCard, id)
};
return (
<ul className="card-list">
{cardData.map(card => (
<Card
onClick={() => onCardClick(card.id)}
key={card.id}
isSelected={selectedCard === card.id}
{...card}
/>
))}
</ul>
)
};
export const CardList = () => (
<List/>
);
const Card = (props) => {
const backColor = props.isSelected? '#F9740E' : '#3FB566';
return (
<div onClick={() => props.onClick()}>
<div style={{backgroundColor: backColor, border: '1px solid darkgreen', color: 'white', padding: 10, marginBottom: 10}}>
<h3>{props.id}</h3>
<h4>{props.title}</h4>
</div>
</div>
);
};
Update
Here is Code SandBox
Not sure why you need to use cloneDeep.
const onCardClick = id => {
if (selectedCard === id) {
setSelectedCard(null);
} else {
setSelectedCard(id);
}
}
I am trying to implement to Show More/Show Less. So far I have was able to bring up a ItemViewer Component where in I display the list of items. For each section there would be Show More / Show Less links. Show More should be visible whenever the number of items is greater than 3 and it should be able to toggle(Show More/ Show Less). When the number of items is less than 3 dont show the link. Also when there is no data display "No data found".
I have come up with a sandbox : https://codesandbox.io/s/pensive-kirch-1fgq3
Can someone help me here?
import React from "react";
import ReactDOM from "react-dom";
import ItemViewer from "./Item";
const item1 = ["i1d1", "i2d2", "i3d3"];
const item2 = ["i2d1", "i2d2", "i2d3", "i2d4"];
const item3 = ["i3d1", "i3d2", "i3d3", "i3d4", "i3d5"];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
item1: [],
item2: [],
item3: []
};
}
componentDidMount() {
this.setState({
item1,
item2,
item3
});
}
render() {
let { item1, item2, item3 } = this.state;
return (
<>
<ItemViewer index="1" item="item1" itemData={item1} />
<ItemViewer index="2" item="item2" itemData={item2} />
<ItemViewer index="3" item="item3" itemData={item3} />
</>
);
}
}
import React, { useState } from "react";
const ItemViewer = props => {
function renderItems(list, itemType) {
if (list && list.length > 0) {
return (
<>
<ul>
{list.map(function(item) {
return <li key={item}>{item}</li>;
})}
</ul>
</>
);
} else {
return <p>No Items Found</p>;
}
}
return (
<div>
<span>
{props.index}: {props.item}
</span>
<div>
Show more
</div>
<div>
Show Less
</div>
<div>{renderItems(props.itemData, props.item, props.itemDetailData)}</div>
</div>
);
};
export default ItemViewer;
You can keep a toggle state inside ItemViewer component, and using that value you can decide to show more or show less.
Codesandbox
import React, { useState } from "react";
const ItemViewer = ({ index, itemData, item }) => {
const [toggle, setToggle] = useState(false);
function renderItems(list) {
if (list && list.length > 0) {
if (list.length > 3 && toggle === false) {
return renderList(list.slice(0, 3), "Show More");
} else if (list.length > 3 && toggle === true) {
return renderList(list, "Show Less");
} else if (list.length === 3) {
return renderList(list, "", false);
}
} else {
return <p>No Items Found</p>;
}
}
function renderList(list, buttonText, showButton = true) {
return (
<div>
<ul>
{list.map(function(item) {
return <li key={item}>{item}</li>;
})}
</ul>
{showButton && (
<div>
<button onClick={toggleHandler}>{buttonText}</button>
</div>
)}
</div>
);
}
const toggleHandler = () => {
setToggle(prev => !prev);
};
return (
<div>
<span>
{index}: {item}
</span>
<div>{renderItems(itemData)}</div>
</div>
);
};
export default ItemViewer;
use component state and onClick listener:
function renderItems(list, itemType) {
if (list && list.length > 0 && this.state.showMore1) {
...
<div>
{" "}
<a onClick={() => this.setState({ showMore1: !this.state.showMore1 )} href="#">Show {this.state.showMore1 ? 'Less' : 'More'}</a>
</div>
Use a handler for show events.
https://codesandbox.io/s/blissful-swirles-rchv0
handleShowEvents(index) {
let showMore = [...this.state.showMore];
showMore[index] = !showMore[index];
this.setState({
...this.state,
showMore: showMore
});
}
Also, use a custom list builder.
itemBuilder() {
let items = [];
for (let i = 0; i < this.state.showMore.length; i++) {
const item = `item${i + 1}`;
if (this.state.showMore[i]) {
items.push(
<ItemViewer
index={i}
item={item}
itemData={this.state.items[i]}
handleShowEvents={this.handleShowEvents}
/>
);
} else {
items.push(
<ItemViewer
index={i}
item={item}
itemData={this.state.items[i].slice(0, 3)}
handleShowEvents={this.handleShowEvents}
/>
);
}
}
return items;
}
Check out this https://codesandbox.io/s/smoosh-shape-vinp6
Let me know if it works for you.
Happy Coding:)