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:)
Related
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>
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);
}
}
Using Downshift, how would one implement settting the inputValue to the value of the currently highlighted item on ArrowUp/ArrowDown while persisting the filtered items until the user manually augments the inputValue?
e.g:
The aforementioned behaviour can be implemented leveraging the stateReducer and useCombobox hook and as follows:
import React, { useState } from "react";
import { render } from "react-dom";
import { useCombobox } from "downshift";
import { items, menuStyles } from "./utils";
function stateReducer(state, actionAndChanges) {
switch (actionAndChanges.type) {
case useCombobox.stateChangeTypes.InputChange:
return {
...actionAndChanges.changes,
userInput: actionAndChanges.changes.inputValue
};
case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
if (!actionAndChanges.changes.inputValue) return actionAndChanges.changes;
return {
...actionAndChanges.changes,
userInput: actionAndChanges.changes.inputValue,
inputValue: actionAndChanges.getItemNodeFromIndex(
actionAndChanges.changes.highlightedIndex
).innerText
};
default:
return actionAndChanges.changes; // otherwise business as usual.
}
}
function DropdownSelect() {
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps
} = useCombobox({
items: inputItems,
stateReducer,
onInputValueChange: ({ userInput, inputValue }) => {
if (userInput === inputValue) {
const filteredItems = items.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase())
);
setInputItems(filteredItems);
}
}
});
return (
<React.Fragment>
<label {...getLabelProps()}>Choose an element:</label>
<div style={{ display: "inline-block" }} {...getComboboxProps()}>
<input {...getInputProps()} />
<button {...getToggleButtonProps()} aria-label="toggle menu">
↓
</button>
</div>
<ul {...getMenuProps()} style={menuStyles}>
{isOpen &&
inputItems.map((item, index) => (
<li
style={
highlightedIndex === index ? { backgroundColor: "#bde4ff" } : {}
}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
))}
</ul>
</React.Fragment>
);
}
render(<DropdownSelect />, document.getElementById("root"));
View the Code sandbox here
today I offer you a new challenge
my problem and the next
in the following code
I map objects in my database
I also map a list of items
and so basically I would like to pass my database and not the Items table
basically I want to do exactly the same thing as in the following code except that instead of using the items array, I would like to be able to use the data array which contains my database
do you have any idea how to do this?
I hope I was precise thanks to you for the help Neff
ps: Sorry for the length of the code i try to do my best to clean a little
import React, { Component } from 'react';
import { CardText, Card,Row, Col, Button } from 'reactstrap';
import axios from 'axios'
import GridLayout from 'react-grid-layout';
import SmsForm from './Sms/SMSForm'
import FruitList from './FruitList'
import './AdminPage.scss'
const UP = -1;
const DOWN = 1;
const entrypoint = process.env.REACT_APP_API_ENTRYPOINT+'/api';
class AdminPage extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 1, name: "orange", bgColor: "#f9cb9c" },
{ id: 2, name: "lemon", bgColor: "#fee599" },
{ id: 3, name: "strawberry", bgColor: "#e06666" }
],
data: [],
}
handleMove = (id, direction) => {
const { items } = this.state;
const position = items.findIndex(i => i.id === id);
if (position < 0) {
throw new Error("Given item not found.");
} else if (
(direction === UP && position === 0) ||
(direction === DOWN && position === items.length - 1)
) {
return; // canot move outside of array
}
const item = items[position]; // save item for later
const newItems = items.filter(i => i.id !== id); // remove item from array
newItems.splice(position + direction, 0, item);
this.setState({ items: newItems });
};
// rest of the component
onHandleChange(event) {
const name = event.target.getAttribute('name');
this.setState({
message: { ...this.state.message, [name]: event.target.value }
});
}
getRandom = async () => {
const res = await axios.get(
entrypoint + "/alluserpls"
)
this.setState({ data: res.data })
}
componentDidMount() {
this.getRandom()
}
render() {
let datas = this.state.data.map(datass => {
const status = JSON.parse(localStorage.getItem("validated-order") || "{}")[datass.id];
return (
<div>
< Col sm="12" key={datass.id} className="wtfFuHereIsForOnlyBackGroundColorForCol12Nice">
<FruitList fruitList={this.state.items} onMove={this.handleMove} />
<GridLayout className="GridlayoutTextOnlyForGridOuiAndHeigthbecauseHeigthWasBug" layout={layout} cols={12} rowHeight={30} width={1200}>
<div key="a">
<Card body className="yoloCardBodyForbackGroundAndRaduisBorderForAdminPageWtf">
<CardText className="cardTextForAdminPageForDataName"> Commande de {datass.name}</CardText>
</Card>
</div>
</ Col>
</div>
)
})
return (
<div> <div>
<Row className="wtfHereIsAgainOnlyForRowAndMarginForAdminPageJustWtf">
<div className="isJustForOnlyPaddingOnRowForAdminPage" >
<div className="navBarGridMenuAdminPage">
<div className="thatIsOnlyForSpaceArroundOnDivAdminPage">
<CardText className="maybeColForAdminPageOuiOui"> Nouvelles commandes </CardText>
</div>
</div>
<div>
{datas}
</div>
</div>
</Row>
</div>
<div className="box">
</div>
</div>
)
}
}
export default AdminPage
here my second components
import React from "react";
const LEFT = -1;
const RIGHT = 1;
class FruitList extends React.Component {
render() {
const { fruitList, onMove } = this.props;
return (
<div style={{ display: "flex" }}>
{fruitList.map(item => (
<div
key={item.id}
style={{
backgroundColor: item.bgColor,
display: "flex"
}}
>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, LEFT)}>←</a>
</div>
<div className="fruitsId">{item.id}</div>
<div className="fruitsName">{item.name}</div>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, RIGHT)}>→</a>
</div>
</div>
))}
</div>
);
}
}
export default FruitList;
To delete particular list do like this-
pass your item(object).
<a onClick={() => onMove(item)}>→</a>
handleMove function
handleMove = (row) => {
let filtered = this.state.items.filter(item=>item.id!==row.id);
this.setState({ items: filtered});
};