categories and sub categories in ReactJS - reactjs

Hello I'm trying to make a sidebar menu using material ui
This sidebar menu contain categories and there sub categories.
I have code this:
What I'm trying to make ?
I take some categories data from my Laravel API, on each categories they can have an object of sub categories, and I collapse this to show the sub categories from the parent category
If I collapse one every one collapse, this is normal because they share the same state, but I don't know how to make one state for each parent categories
const categoriesList = this.state.categories.map((item, k) => {
return (
<React.Fragment key={k}>
<ListItem button onClick={this.handleClick}>
<ListItemText primary={item.label} />
{item.category !== null ? this.state.open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{item.category !== null ? <>
<Collapse in={this.state.open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.category.map((ite,l) => <ListItem key={l} button className={classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
)
})
``

I would recommend that you create a component for each category. So instead you would have:
const categoriesList = this.state.categories.map((item, k) => {
return <Category key={k} {...item} />;
});
And elsewhere in your code you'd have another Component:
const Category = (props) => {
const [open, setOpen] = useState(false);
function handleClick(e) { ... }
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemText primary={props.label} />
{props.category !== null ? open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{item.category !== null ? <>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{props.category.map((ite,l) => <ListItem key={l} button className={classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
);
}
Then the state is contained with the Category component, and you can keep your container component simple.
[note] I prefer function components, so used that here, but a Class Component wouldn't change this answer.

This is my parent component with hook
first time I use hooks
import React, {useState} from 'react';
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import ExpandLess from "#material-ui/icons/ExpandLess";
import ExpandMore from "#material-ui/icons/ExpandMore";
import Collapse from "#material-ui/core/Collapse";
import List from "#material-ui/core/List";
export function Category(props){
const [open, setOpen] = useState(false);
let handleClick = () => {
setOpen(!open)
}
return (
<React.Fragment key={props.id}>
<ListItem button onClick={handleClick}>
<ListItemText primary={props.categorie.label} />
{props.category !== null ? open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{props.category !== null ? <>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{props.category.map((ite,l) => <ListItem key={l} button className={props.classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
)
}

Related

Expand collapse (material-ui) only one at a time - React

I have table that I can expand Collapse material-ui. Everything is worked perfectly.
But there is one thing I want to update that when I click to expand there is only one expand collapse opened at a time.
const InvoiceInfo = ({el}) => {
const [open, setOpen] = React.useState(false)
}
function handleExpand() {
setOpen(!open)
}
return (
<>
<TableRow key={el.id}>
<TableCell>
{el.name}
</TableCell>
<TableCell>
<IconButton onClick={handleExpand}>
{open ? (
<KeyboardArrowUp fontSize="large" />
) : (
<KeyboardArrowDown fontSize="large" />
)}
</IconButton>
</TableCell>
</TableRow>
{
<TableRow>
<Collapse in={open} timeout="auto" unmountOnExit>
<div>
EXPAND ROW
</div>
</Collapse>
</TableRow>
}
</>
)
Above is my code looks like.
Any advice is very meaningful to me. Thank you so much.
You can use a simple state based on the index of an array or a given number / value:
const [isOpenCollapse, setIsOpenCollapse] = useState(null);
const handleOpen = (clickedIndex) => {
if (isOpenCollapse === clickedIndex) {
setIsOpenCollapse(null);
} else {
setIsOpenCollapse(clickedIndex);
}
};
[...]
return (
<List>
{arr.map((elm, index) => {
return (
<>
<ListItem key={elm.ID} onClick={() => handleOpen(index)} id="listItem">
{isOpenCollapse === index ? <ExpandLess /> : <ExpandMore />}
<Box>
<Typography>{elm.label}</Typography>
</Box>
</ListItem>
<Collapse in={isOpenCollapse === index} timeout="auto" unmountOnExit>
<p>PUT SOMETHING HERE</p>
</Collapse>
</>
);
})}
</List>;
)

Solve: Use a function from the child component to assign a value into the props?

In my parent component, there's a state that stores the selected task id from the child component. I used the props to pass the data from child to parent component. But my problem is I will use a function that will generate the selected task id inside the child component. The result that i want is if the user selects a button it will trigger the function that will generate the task id. Then it will store the data into props that will be pass to the parent component. The reason why the i will used a function instead of using onClick={e => e.currentTarget.getAttribute('taskkd')} I will also need to pass other props that will display the dialog box.
Parent Component
const Home = () => {
let userInfo = localStorage.getItem('userData');
const [taskDeleteId, setTaskDeleteId] = useState();
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
console.log(taskDeleteId);
}, [history, taskDeleteId]);
return (
<>
<Grid item xs={12} sm={8} className={baseClasses.mainTaskContainer}>
<TasksList
deleteId={(deleteId) => setTaskDeleteId(deleteId)}
showPrompt={(showPrompt) => setShowPrompt(showPrompt)}
/>
<PromptComponent showPrompt={showPrompt} shownewArr={newArr} />
</Grid>
</Grid>
</>
);
};
Child Component
const TasksList = ({ deleteId, showPrompt }) => {
const selectedTask = (e) => {
deleteId = e.currentTarget.getAttribute('taskid'); <--- the task id that i need to pass into my parent component
};
return (
<>
<Container
component='div'
maxWidth='xl'
className={classes.taskContainer}
><Container
component='div'
maxWidth='xl'
className={classes.taskContainer}
>
<Container
component='div'
maxWidth='xl'
className={classes.todoContainer}
onDragOver={(e) => onDragOverTask(e)}
onDragLeave={(e) => onDragLeaveTask(e)}
onDrop={(e) => onDropTask(e)}
>
<Typography variant='h6' className={classes.containerTitle}>
<span>To do</span>
<span>{taskTodo.length}</span>
</Typography>
{taskTodo.map((currentTask, index) => {
return (
<Card
variant='outlined'
className={classes.cardTask}
key={index}
style={{ background: '#f3f3f3', marginTop: '1rem' }}
onDragStart={(e) => onDragStartTask(e, currentTask._id)}
draggable
>
{currentTask.desc && (
<Collapse
in={expanded === `todo-panel_${index}`}
timeout='auto'
unmountOnExit
className={classes.collapsePanel}
color='primary'
>
<CardContent className={classes.descPrevContainer}>
<Typography
variant='body1'
className={classes.text}
dangerouslySetInnerHTML={createMarkup(
textTruncate(currentTask.desc, 50)
)}
></Typography>
</CardContent>
</Collapse>
)}
<CardActions
disableSpacing
className={classes.bottomActionsContainer}
>
<Tooltip title='Delete'>
<IconButton
aria-label='delete'
className={classes.BottomDelete}
taskid={currentTask._id}
onClick={(() => showPrompt(true), selectedTask)} <--- props
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title='Edit'>
<IconButton
aria-label='edit'
className={classes.BottomEdit}
taskid={currentTask._id}
>
<EditIcon />
</IconButton>
</Tooltip>
</CardActions>
</Card>
);
})}
</Container>
</>
);
};

How can I use show component inside a ListItemText?

I used the ListItemText to display the list and my goal is to use the show component in the react-admin when each row of the list is clicked, but the function related to the display is not executed? How should it be done?
Contacts.js
/// --- List ---
export const ContactList = (props) => {
const classes = useStyles();
return (
<List className={classes.list} {...props} pagination={false} exporter={false} filters={<ContactFilter/>}>
<ContactSimpleList/>
</List>
)
};
/// --- Child list ---
const ContactSimpleList = () => {
const {ids, data} = useListContext();
const handleClick = (id) => {
ShowContact(id);
}
return (
<>
{ids.map(id => (
<ListItem key={id} button>
<ListItemAvatar>
<Avatar alt="Profile Picture" src={data[id].person}/>
</ListItemAvatar>
<ListItemText primary={data[id].name} onClick={() => handleClick(id)}/>
</ListItem>
))}
</>
);
}
/// --- Show ---
export const ShowContact = (props) => (
<Show {...props} actions={<ShowActionsOnTopToolbar/>} title={<ContactTitle/>}>
<SimpleShowLayout>
<TextField source="id"/>
<TextField source="name"/>
<TextField source="numbers.number" label="Number"/>
<TextField source="numbers.type" label="Type Call"/>
</SimpleShowLayout>
</Show>
);
This is what you need to do; replace Typography with your component
<ListItemText
disableTypography
primary={
<Typography>Pedroview</Typography>
}
/>

Loading React component from another file

I am using the React Material-UI library. I have a search 'filter' on the main/parent page, which should pop open a Drawer that is located in a separate file.
I understand how this all works within one file, but I cannot understand how to break this into separate files (obviously for code clarity intent). I also cannot tell how I get the 'variables' out of the Drawer for items I select. Below I have pasted my two files, whereby I would like to get the 'onclick' in the parent to launch the Drawer in the child file. Any assistance would be excellent!
My parent file:
return (
<Autocomplete
id="search"
...
renderInput={(params) => (
<TextField
{...params}
label="Search for an item"
variant="outlined"
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<IconButton color="primary" aria-label="filters" component="span">
<TuneRoundedIcon onClick={ handleFilterOpen } />
</IconButton>
<Filter open={ openFilter} />
</InputAdornment>
),
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
fullWidth
/>
)}
And this is the Filter.tsx (my child file):
export default function Filter() {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<Drawer
className={classes.drawer}
variant="persistent"
anchor="right"
open={open}
classes={{
paper: classes.drawerPaper,
}}
>
<div className={classes.drawerHeader}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</div>
<Divider />
<List>
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
<ListItem button key={text}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem button key={text}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
</Drawer>
);
}
You need to pass the open state down to the child component via props. The child component should not have an "open" state. I've simplified this for you for better understanding:
const Parent = () => {
const [open, setOpen] = React.useState(false);
const toggleOpen = () => {
setOpen(!open)
}
const customFunction(valueFromChildComponent) {
alert(valueFromChildComponent); // this alerts "myValue" since "myValue" is passed from child component
}
return (
<Child isOpen={open} myCustomFunction={customFunction} />
);
}
Child component
const Child = props => {
return (
console.log(props.isOpen) // props.isOpen contains the value passed down from the parent
<button onClick={props.myCustomFunction("myValue")}>test Button</button>
);
}
I've also added an example how you could pass up a value form your child component. in my case i passed up a string but you could for example pass up the selected value of a drop down or basically whatever you want.

setState is not being called onClick

Working with material-ui v3.9.4, react-router v3. Created a left nav bar with a List containing Link items to redirect to another page. For some reason when I click on an item, the page is redirected, but the state is not updated until the second click to show the active item. Any idea as to why that is? Code below. I've console logged the setState change, which doesnt print until the second click.
class LeftNavigation extends Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: 0,
sidebarCollapsed: false,
}
}
handleListItemClick = (event, index) => {
console.log("CLICKED"); //prints first click
this.setState({ selectedIndex: index }, () => {
console.log("SELECTED INDEX:: ", this.state.selectedIndex) //prints second click
});
}
handleCollapse = () => {
this.setState({ sidebarCollapsed: !this.state.sidebarCollapsed })
}
render() {
const { classes } = this.props;
return (
<div className={this.state.sidebarCollapsed ? classes.rootCollapsed : classes.root}>
<List component="nav">
<ListItem
button
component={props => <Link to="./pageOne" {...props} />}
selected={this.state.selectedIndex === 0}
onClick={(event) => this.handleListItemClick(event, 0)}
>
<ListItemIcon>
<CalendarIcon />
</ListItemIcon>
<ListItemText classes={{ primary: classes.listText }} primary="Page One" />
</ListItem>
<ListItem
button
onClick={(event) => this.handleListItemClick(event, 1)}
component={props => <Link to="./pageTwo" {...props} />}
selected={this.state.selectedIndex === 1}
>
<ListItemIcon>
<MailIcon />
</ListItemIcon>
<ListItemText classes={{ primary: classes.listText }} primary="Page Two" />
</ListItem>
<ListItem
button
component={props => <Link to="./pageThree" {...props} />}
selected={this.state.selectedIndex === 2}
onClick={(event) => this.handleListItemClick(event, 2)}
>
<ListItemIcon>
<NotesIcon />
</ListItemIcon>
<ListItemText classes={{ primary: classes.listText }} primary="Page Three" />
</ListItem>
</List>
<div
onClick={this.handleCollapse}>
{this.state.sidebarCollapsed ? <ChevronRight /> :
<div><ChevronLeft /><p>Collapse sidebar</p></div>}
</div>
</div>
)
}
}
export default withStyles(styles)(LeftNavigation );

Resources