This is my scenario
<List>
{mainTags.map((mainTagItem) => {
return (
<ListItem onClick={() => { setMainTag(mainTagItem.tag.tagId) }} button className={classes.mainTagItem}>
<div className={classes.mainTagCircle}></div>
<ListItemText
primary={mainTagItem.tag.name}
/>
</ListItem>
)
})}
</List>
when i click on my ListItem ( that becomes selected ) i want the element <div className={classes.mainTagCircle}> has an active class
For Example:
<div classes={{ root: !!listItemSelected ? classes.mainTagCircleActive : classes.mainTagCircle, }}></div>
I have already a method onClick in my ListItem, how can i apply this logic?
given that you have a mainTag state you could compare with your element tagId to define which class to select. If it's the same as your state then active class wil be returned:
<div className={
mainTag === mainTagItem.tag.tagId
? classes.mainTagCircleActive
: classes.mainTagCircle}>
</div>
Solution with a library
You could try with this library clsx. Then do something like this:
function Component() {
const [mainTag, setMainTag] = useState(null);
return (
<List>
{mainTags.map((mainTagItem) => {
return (
<ListItem onClick={() => { setMainTag(mainTagItem.tag.tagId) }} className=
{clsx([classes.mainTagItem, mainTag === mainTagItem.tag.tagId ? :
'activeClass': 'defaultClass' ])}>
<div className={classes.mainTagCircle}></div>
<ListItemText
primary={mainTagItem.tag.name}
/>
</ListItem>
)
})}
</List>
)
}
Solution without libraries
function Component() {
const [mainTag, setMainTag] = useState(null);
return (
<List>
{mainTags.map((mainTagItem) => {
return (
<ListItem onClick={() => { setMainTag(mainTagItem.tag.tagId) }} className={
mainTag === mainTagItem.tag.tagId
? classes.mainTagCircleActive
: classes.mainTagCircle}
/>
</ListItem>
)
})}
</List>
)
}
Related
Im having troubles to expand and contract a Collapse Component from MaterialUI since Im mapping and array and iterating the same component, when i press the collapse button, all components expands/contracts at the same time ( I suppose that Im not providing an identifier to point where the collapse function should be used),Im currently Using an State to control the collapse action:
const [expanded, setExpanded] = useState(false);
This is the return where I iterate the component using map on RecetasAll object,
return (
<React.Fragment key={RecetasAll.id}>
<Card className="searchItem" sx={{ maxWidth: 345 }}>
<CardHeader
action={<IconButton aria-label="settings"></IconButton>}
title={RecetasAll.titulo}
/>
<h4
className="Dieta"
style={{
backgroundColor: color(RecetasAll.Tiporeceta.tipoReceta),
}}
>
{RecetasAll.Tiporeceta.tipoReceta}
</h4>
<span className="Calorias">{RecetasAll.informacionNutricional}</span>
<CardMedia
component="img"
height="194"
image={RecetasAll.imagen}
alt="Paella dish"
/>
<CardContent>
{RecetasAll.Productos.map((Productos) => {
return (
<React.Fragment key={Productos.id}>
<Typography variant="body2" color="text.secondary">
{Productos.producto}
</Typography>
</React.Fragment>
);
})}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent id={RecetasAll.id}>
<Typography paragraph>Preparacion:</Typography>
<Typography paragraph>{RecetasAll.pasos}</Typography>
<Button
href="#contained-buttons"
variant="contained"
onClick={handleSearch}
>
Ver mas
</Button>
</CardContent>
</Collapse>
</Card>
</React.Fragment>
);
});
return <>{itemRecetas}</>;
}
Im triying to set an id property to the CardContent since its the child of the Collapse component
id={RecetasAll.id}
this is the function Im using to expand or collapse but I dont know how to get the id properly to compare its value with expanded state:
const handleExpandClick = (e) => {
let clickedItemId = e.currentTarget.id;
if (expanded === clickedItemId) {
setExpanded(!expanded);
} else {
setExpanded(clickedItemId);
}
};
You could refactor every card into a new component and that way you can have a state to open/close the individual card. When iterating you can pass in the RecetasAll.
const MyCard = ({ RecetasAll }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => {
setIsExpanded(prevIsExpanded => !prevIsExpanded);
};
return (
...
<ExpandMore
expand={isExpanded}
onClick={toggleExpanded}
aria-expanded={isExpanded}
>
...
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
...
);
};
If you don't want to use a new component you could store all the ids of the expanded cards in a state. Based on if the id is in the array the card will be expanded or collapsed.
const [expandedIds, setExpandedIds] = useState([]);
const toggleExpanded = (id) => {
setExpandedIds((prevExpandedIds) => {
// if id is already in array remove
if (prevExpandedIds.includes(id))
return prevExpandedIds.filter((i) => i !== id);
// else add to array
return [...prevExpandedIds, id];
});
};
return (
...
<ExpandMore
expand={expandedIds.includes(RecetasAll.id)}
onClick={() => toggleExpanded(RecetasAll.id)}
aria-expanded={expandedIds.includes(RecetasAll.id)}
>
...
<Collapse in={expandedIds.includes(RecetasAll.id)} timeout="auto" unmountOnExit>
...
)
I want the category details in the parent categoryHandler function
from the child component. I don't know where to place this
props.categoryHandler function in the child component so that when it
is clicked I should get the details to the parent categoryHandler
function.
PARENT COMPONENT:
const categoryHandler = (catg) => {
console.log(catg);
}
<div className="categoryBox">
<Category categories={categories} categoryHandler={() => categoryHandler} />
</div>
CHILD COMPONENT:
export default function Category({ categories }) {
if (categories.length) {
const menu = recursiveMenu(categories);
return menu.map((item, key) => <MenuItem key={key} item={item} />);
} else {
return <div>No Menus</div>
}
}
const MenuItem = ({ item }) => {
const Component = hasChildren(item) ? MultiLevel : SingleLevel;
return <Component item={item} />;
};
const SingleLevel = ({ item }) => {
return (
<ListItem button>
<ListItemText className="category-link child" primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemText className="category-link parent" primary={item.title} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
Your approach in the code is right, just you have to modify two thing to achieve what you are expecting.
In the parent component you have to modify passing the function as a props like this:
categoryHandler={categoryHandler}
In the child component you have to catch the function while destructuring the props and call in on the both list item with the single item as function parameter:
add the function in props destructuring and pass the function as another props to MenuItem
export default function Category({ categories, categoryHandler }) {
if (categories.length) {
const menu = recursiveMenu(categories);
return menu.map((item, key) => <MenuItem categoryHandler={categoryHandler} key={key} item={item} />);
} else {
return <div>No Menus</div>
}
}
Now again pass the function props to Single And MultiLevel List and call the function on both place:
const MenuItem = ({ item, categoryHandler }) => {
const Component = hasChildren(item) ? MultiLevel : SingleLevel;
return <Component item={item} categoryHandler={categoryHandler} />;
};
const SingleLevel = ({ item, categoryHandler }) => {
return (
<ListItem button onClick={handleClick}>
<ListItemText className="category-link child" primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item, categoryHandler}) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemText className="category-link parent" primary={item.title} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
This solution should work fine!
When you call your function passed as a prop, you can pass data from a child to parent component.
Parent Component:
const categoryHandler = (catg) => {
console.log(catg);
}
<div className="categoryBox">
<Category categories={categories} categoryHandler={categoryHandler} />
</div>
Child Component:
export default function Category(props) {
props.categoryHandler(data);
}
You can need to pass the parameter to the function in your parent component like this:
<Category categories={categories} categoryHandler={categoryHandler} />
You need to pass the prop categoryHandler all the way to the SingleLevel like this:
categoryHandler={categoryHandler}
You can need to add onClick to the ListItem in your SingleLevel component with item parameter like this:
<ListItem button onClick={() => categoryHandler(item)}>
You can take a look at this sandbox for a live working example of this solution.
I'm designing the frontend of a chat application and this is the Conversations component.
The problem is on line 87 and I've highlighted the line. 'conversations' is an array so I'm unable to figure out the issue here. I'm actually quite new to react so apologies if it's something silly. Thanks!
const Conversations = (props) => {
const classes = useStyles();
const [conversations, setConversations] = useState([]);
const [newConversation, setNewConversation] = useState(null);
const getConversations = useGetConversations();
useEffect(() => {
getConversations().then((res) => setConversations(res));
}, [newConversation]);
useEffect(() => {
let socket = socketIOClient(process.env.REACT_APP_API_URL);
socket.on("messages", (data) => setNewConversation(data));
return () => {
socket.removeListener("messages");
};
}, []);
return (
<List className={classes.list}>
<ListItem
classes={{ root: classes.subheader }}
onClick={() => {
props.setScope("Global Chat");
}}
>
<ListItemAvatar>
<Avatar className={classes.globe}>
<LanguageIcon />
</Avatar>
</ListItemAvatar>
<ListItemText className={classes.subheaderText} primary="Global Chat" />
</ListItem>
<Divider />
{conversations && (
<React.Fragment>
// 87 {conversations.map((c) => ( // error on this line
<ListItem
className={classes.listItem}
key={c._id}
button
onClick={() => {
props.setUser(handleRecipient(c.recipientObj));
props.setScope(handleRecipient(c.recipientObj).name);
}}
>
<ListItemAvatar>
<Avatar>
{commonUtilites.getInitialsFromName(
handleRecipient(c.recipientObj).name
)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={handleRecipient(c.recipientObj).name}
secondary={<React.Fragment>{c.lastMessage}</React.Fragment>}
/>
</ListItem>
))}
</React.Fragment>
)}
</List>
);
};```
I think you need to set data which you are getting in getConversations.
setConversations(res.data)
I want to prevent that when I click on several items, I have several items (expand: "true")
I would like that when I click on a new item, the old one goes back to its original state (expand: "false")
However, I did not find any property in the documentation to handle this.
https://material-ui.com/components/tree-view/
{stoMenu && (
<TreeView
style={layout.menu}
defaultCollapseIcon={<KeyboardArrowUpIcon />}
defaultExpandIcon={<KeyboardArrowDownIcon />}
>
{stoMenu.root.children.map(menu => {
return (
<TreeItem
key={menu.nodeId}
nodeId={menu.nodeId}
label={
<ListItem
style={layout.menuListItem}
className={menu.iconCls}
>
<ListItemText
style={layout.menuText}
primary={menu.text}
onClick={() => {
if (menu.id === '/accueil') {
this.props.history.push(menu.id);
}
}}
/>
</ListItem>
}
>
{menu.children.map(child => {
return (
<TreeItem
// style={layout.subMenuText}
className={classes.subMenu}
key={child.nodeId}
nodeId={child.nodeId}
label={child.text}
onClick={() => {
if (child.id) {
this.props.history.push(child.id);
}
}}
/>
);
})}
</TreeItem>
);
})}
</TreeView>
)}
I wish to add spinner animation after clicking on button, when get response, spinner is supposed to disappear. So far works fine but the problem is that I render list with many elements and every element has own delete button, while clicking on one, animation is added to all elements of the list. I wish it to appear only once, next to this particular clicked element of the list.
const displayCertificateList = (
classes,
mainStatus,
handleDeleteSingleCertificate,
animateDelete
) => {
return mainStatus.map((el, i) => {
return (
<div className={classes.certificatesListContainer} style={{border:'none'}}>
<List key={i} style={{padding: '10px'}}>
<ListItem style={{ padding: "0 0 0 20px" }}>
<ListItemText
className={classes.certificatesList}
primary={
<Typography type="body2" style={{ fontWeight: "bold" }} className={classes.certificatesListFont}>
Valid until:
</Typography>
}
secondary={
<Typography
type="body2"
className={classNames(
classes.certificatesListSecondArgument,
classes.certificatesListFont,
el.expiresIn > 90 ? classes.green : classes.red
)}
>
{el.validUntil.slice(0,9)} ({el.expiresIn} days)
</Typography>
}
/>
</ListItem>
</List>
<div className={classes.certificatesBtn}>
<Button
variant="contained"
size="small"
color="secondary"
className={classes.button}
onClick={() => {
if (
window.confirm(
`Are you really sure?
)
)
handleDeleteSingleCertificate(el, i);
}}
>
<DeleteIcon className={classes.leftIcon} />
Delete
</Button>
<div style={{left: '-50%',top: '30%'}} className={classNames(animateDelete ? classes.spinner : null)}></div>
</div>
</div>
);
});
} else {
return (
<div>
<Typography component="h1" variant="h6">
The applet is not innitialized, please initialize it first
</Typography>
</div>
);
};
And in parent component:
handleDeleteSingleCertificate = (el, i) => {
this.setState({animatingDelete: true})
this.make_call(
this.state.selected,
(res) => {
console.log(res)
this.setState({animatingDelete: false})
}
)
}
And pass it like this:
{this.state.view === 'certificates' && this.state.certificates && displayCertificates(classes, fakeData, this.handleDeleteSingleCertificate, this.state.animatingDelete)}
I suggest to make displayCertificateList function component to stateful component and store the animatingDelete in it - `cause it is the state of that particular item in deed.
class ListItem extends React.Component {
state = {
isDeleting: false
}
handleDelete = () => {
const { onDelete, id } = this.props;
onDelete(id);
this.setState({
isDeleting: true
})
}
render(){
const { isDeleting } = this.state;
return (
<li>
<button onClick={this.handleDelete}>Delete {isDeleting && '(spinner)'}</button>
</li>
)
}
}
class List extends React.Component {
state = {
listItems: [
{id: 1},
{id: 2}
]
}
handleDelete = id => {
console.log('delete ' + id);
// do the async operation here and remove the item from state
}
render(){
const { listItems } = this.state;
return (
<ul>
{listItems.map(({id}) => (
<ListItem id={id} key={id} onDelete={this.handleDelete} />
))}
</ul>
)
}
}
ReactDOM.render(<List />, document.getElementById('root'));
<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>
<div id="root" />
In my opinion, it's better to use count instead of animatingDelete to mark. You can plus 1 when click on the delete button and then when it's done minus 1. when count equals to 0, hide spining otherwise show it.