Three Rows Table using Material-UI and React - html

I need to achieve something like this in the picture.
I need to create a table with three rows, where the third row is below the first and second rows, and the width of the third row is the combined width of the first and second rows.
CODESANDBOX: CLICK HERE FOR CODESANDBOX
CODE
const CustomTable = () => {
const handleSubmit = () => {};
return (
<TableContainer component={Paper}>
<Formik
initialValues={[
{
id: 1,
attribute: "",
thirdRow: ""
}
]}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form>
<FieldArray
name="rows"
render={(arrayHelpers) => (
<React.Fragment>
<Box>
<Button
variant="contained"
type="submit"
startIcon={<AddIcon />}
onClick={() =>
arrayHelpers.unshift({
id: Date.now(),
attribute: "",
ruleId: "",
thirdRow: ""
})
}
>
Add New
</Button>
</Box>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell>
<span />
Rule ID
</TableCell>
<TableCell colSpan={2}>Third Row</TableCell>
</TableRow>
</TableHead>
<TableBody>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</TableBody>
</Table>
</React.Fragment>
)}
/>
</Form>
)}
</Formik>
</TableContainer>
);
};
export default CustomTable;

You could tweak each "row" to really render 2 rows where the second row's column spans 2 column widths.
demo.js
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow
sx={{
"th": { border: 0 }
}}
>
<TableCell>Attribute</TableCell>
<TableCell>Rule ID</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2}>Third Row</TableCell>
</TableRow>
</TableHead>
<TableBody>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</TableBody>
</Table>
CustomTableRow
const CustomTableRow = ({ row, index }) => {
return (
<>
<TableRow
sx={{
"th, td": { border: 0 }
}}
>
<TableCell component="th" scope="row">
<FastField
name={`rows.${index}.attribute`}
component={TextField}
fullWidth
/>
</TableCell>
<TableCell>
<FastField
name={`rows.${index}.ruleId`}
component={TextField}
fullWidth
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2}>
<FastField
name={`rows.${index}.thirdRow`}
component={TextField}
fullWidth
/>
</TableCell>
</TableRow>
</>
);
};

I prefer to use grid layout. You won't then use Table elements but will then want to give the role attribute for accessibility.
The nice thing about using grid layout is that you get a lot of flexibility so you can then say check for [theme.breakpoints.down("md")] and adjust your gridTemplateAreas accordingly.
So, you would do something like the following to get all the 3 rows stacked one over the other.
[theme.breakpoints.down("md")]: {
gridTemplateColumns: "1fr",
gridTemplateAreas: `"upper1"
"upper2"
"bottom"`,
gap: "0px",
}
The other benefit of using gridArea is that the display is visual so it is easy to see as you are designing your layout
I forked your codesanbox and the link and it is at https://codesandbox.io/s/dry-tdd-vv6z1s?file=/demo.js
The complete code is given below.
import React from "react";
import Table from "#mui/material/Table";
import TableBody from "#mui/material/TableBody";
import TableCell from "#mui/material/TableCell";
import TableContainer from "#mui/material/TableContainer";
import TableHead from "#mui/material/TableHead";
import TableRow from "#mui/material/TableRow";
import Paper from "#mui/material/Paper";
import { Box, Button } from "#mui/material";
import AddIcon from "#mui/icons-material/Add";
import { FieldArray, Form, Formik } from "formik";
import CustomTableRow from "./CustomTableRow";
import { styled } from "#mui/material/styles";
const Wrapper = styled(Box)(({ theme }) => ({
display: "grid",
gridTemplateColumns: "1fr 1fr",
gridTemplateAreas: `"upper1 upper2 "
"bottom bottom"`,
gap: "0px"
}));
const Upper1 = styled(Box)(({ theme }) => ({
gridArea: "upper1"
}));
const Upper2 = styled(Box)(({ theme }) => ({
gridArea: "upper2"
}));
const Bottom = styled(Box)(({ theme }) => ({
gridArea: "bottom"
}));
const CustomTable = () => {
const handleSubmit = () => {};
return (
<TableContainer component={Paper}>
<Formik
initialValues={[
{
id: 1,
attribute: "",
ruleId: "",
thirdRow: ""
}
]}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form>
<FieldArray
name="rows"
render={(arrayHelpers) => (
<React.Fragment>
<Box>
<Button
variant="contained"
type="submit"
startIcon={<AddIcon />}
onClick={() =>
arrayHelpers.unshift({
id: Date.now(),
attribute: "",
ruleId: "",
thirdRow: ""
})
}
>
Add New
</Button>
</Box>
<Box aria-label="simple table">
<Wrapper>
<Upper1 style={{ border: "2px blue solid" }}>
Attribute
</Upper1>
<Upper2 style={{ border: "2px blue solid" }}>
Rule ID
</Upper2>
<Bottom style={{ border: "2px blue solid" }}>
Third Row
</Bottom>
</Wrapper>
<Box>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</Box>
</Box>
</React.Fragment>
)}
/>
</Form>
)}
</Formik>
</TableContainer>
);
};
export default CustomTable;

Related

Cypress check if the sort the grid by order

I am trying to sort images by order, i have the button new and old. the [data-cy="list-file"] is the wrap for all images and [data-cy=fileCard-list] represent each one image.
I want after clicking the button old for example to be able the check with cypress if the sort is working perfectly.
import { sortFilteredComments } from './../../../../lib/vmr';
import loginData from '../../../fixtures/login.json';
/// <reference types='cypress' />
describe('upload multiple photos', () => {
before(() => {
cy.clearLocalStorageSnapshot();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
it('login', () => {
cy.get('[data-cy=projects-card-table]').contains('E2E-TEST').click();
cy.get('[data-cy="header-search-bar"]').click();
cy.wait(3000).get('[data-cy=file-order-old]').click();
cy.get('[data-cy="list-file"]').should('be.visible');
cy.get('[data-cy=each-item-file]').should('be.visible');
});
});
Here is the file with sort method
`import Card from '#material-ui/core/Card';
import CardMedia from '#material-ui/core/CardMedia';
import Grid from '#material-ui/core/Grid';
import { makeStyles } from '#material-ui/core/styles';
import Typography from '#material-ui/core/Typography';
import { IFetchedFiles } from 'api';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { formatFullDate } from '~/lib/date';
import MimeIcon from '../../MimeIcon';
import { COLORS } from '~/lib/theme';
const useStyles = makeStyles(() => ({
card: {
backgroundColor: '#F1F3F3',
height: '400px',
width: '288px',
margin: '8px'
},
cardMedia: {
height: '200px',
margin: '10px',
cursor: 'pointer',
position: 'relative'
},
cardMediaImgContain: {
backgroundSize: 'contain'
},
excelTitle: {
backgroundColor: COLORS.white,
bottom: 0,
color: 'black',
fontSize: 14,
fontWeight: 'bold',
position: 'absolute',
padding: 4,
right: 0
},
group: {
marginRight: 20
},
groupWrap: {
display: 'flex',
padding: '0px 10px'
},
projectLabel: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: '11px',
padding: '0px 10px'
},
title: {
fontSize: 14
},
titleWithMarginTop: {
fontSize: 14,
marginTop: 12
},
fileData: {
fontSize: 14,
fontWeight: 'bold'
},
fileDataMarginTop: {
fontSize: 14,
fontWeight: 'bold',
marginTop: 12
},
iconsContainer: {
display: 'flex',
gap: '4px',
padding: '4px'
}
}));
const iconSize = 24;
interface IProps {
file: IFetchedFiles['files'][number];
organizationName: string;
getImageUrl: () => any;
imageMime: boolean;
excelMime: boolean;
pdfMime: boolean;
tiffMime: boolean;
}
const FileCard: React.VFC<IProps> = ({
file,
organizationName,
getImageUrl,
imageMime,
excelMime
}) => {
const classes = useStyles();
const { t, i18n } = useTranslation();
const router = useRouter();
return (
<Grid **data-cy='each-item-file'** key={file.file.id}>
<Card className={classes.card}>
<CardMedia
className={clsx(classes.cardMedia, {
[classes.cardMediaImgContain]: excelMime
})}
image={getImageUrl()}
onClick={() => {
const photoUrl = `/orgs/${organizationName}/projects/${file.project.id}/photos/${file.file.id}`;
router.push(photoUrl);
}}
>
<div className={classes.iconsContainer}>
{file.file.mime && (
<MimeIcon mime={file.file.mime} iconSizeMime={iconSize} />
)}
</div>
{excelMime && (
<div className={classes.excelTitle}>{file.file.name}</div>
)}
</CardMedia>
<Typography variant='h6' className={classes.projectLabel}>
{file.project.label}
</Typography>
<div className={classes.groupWrap}>
<div className={classes.group}>
{file.material_group.label && (
<Typography variant='h6' className={classes.title}>
{t('searchFile.fileList.materialGroup')}
</Typography>
)}
<Typography variant='h6' className={classes.title}>
{t('searchFile.fileList.material')}
</Typography>
<Typography variant='h6' className={classes.title}>
{t('searchFile.fileList.process')}
</Typography>
<Typography variant='h6' className={classes.titleWithMarginTop}>
{imageMime
? t('searchFile.fileList.date')
: t('file.lastModified')}
</Typography>
<Typography variant='h6' className={classes.title}>
{imageMime
? t('searchFile.fileList.photographer')
: t('searchFile.fileList.registrant')}
</Typography>
</div>
<div>
{file.material_group.label && (
<Typography
variant='h6'
color='primary'
className={classes.fileData}
>
{file.material_group.label}
</Typography>
)}
<Typography
variant='h6'
color='primary'
className={classes.fileData}
>
{file.material.label ?? t('searchFile.fileList.uncategorized')}
</Typography>
<Typography
variant='h6'
color='primary'
className={classes.fileData}
>
{file.process.label ?? t('searchFile.fileList.uncategorized')}
</Typography>
<Typography
variant='h6'
className={classes.fileDataMarginTop}
color='primary'
>
{`${formatFullDate(new Date(file.file.date), i18n.language)}`}
</Typography>
<Typography
variant='h6'
color='primary'
className={classes.fileData}
>
{file.user.display_name ?? ''}
</Typography>
</div>
</div>
</Card>
</Grid>
);
};
export default FileCard;
`
It's a little bit confusing because it's not a table and i am working on react.js and typescript. Here everything is working perfectly but just the last assertion is where i have no clue.
Notes: you can see that [data-cy="list-file"] represent the whole container and [data-cy=fileCard-list] represent each file inside of the container.
I think we need more details about the sorting method you are using. In a table we could check the aria-sort property. Do you have any similar way to check it?
Another way could be iterating over all the [data-cy=fileCard-list] and checking if the parameter you are using to order them is correctly sorted. I don't understand what you mean with that last assertion cy.get('[data-cy="list-file"]').should('be.visible');

How to keep a dynamic table row when it has no value?

I have this code :
const HistoricalGrid = ((props) => {
return (
<div className="main-table">
<DataTable rows={props.selectedFile} headers={headers}>
{({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
<Table {...getTableProps()}>
<TableHead>
<TableRow>
{headers.map((header) => (
<TableHeader {...getHeaderProps({ header })}>
{header.header}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{props.selectedFile.map((row) => (
<TableRow rows="4" {...getRowProps({ row })}>
<TableCell key={row.id} >{row.name}</TableCell>
<TableCell key={row.id}>{row.type}</TableCell>
<TableCell key={row.id}>{new Date().toString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</DataTable>
But, I only get the rows data when a button is clicked, how can I keep the rows even if has no value ?
you can check the array length and use a ternary :
{props.selectedFile.length
? props.selectedFile.map((row) => (
...
))
: <TableRow>No result</TableRow>
}
const generateNRows = nb => {
const rows = [];
for($i = 0; $i < $nb; $i++){
rows.push(<tr/>);
}
return rows;
}
const nowDate = new Date();
return (...
{props.selectedFile.map((row) => (
<TableRow {...row} key={row.id} >
<TableCell>{row.name}</TableCell>
<TableCell>{row.type}</TableCell>
<TableCell>{nowDate.toString()}</TableCell>
</TableRow>
))}
{props.selectedFile.length < 4 && (
<>
{generateNRows(4 - props.selectedFile.length)}
</>
)}

display array data through map in react table

I am trying to display array data in a table for my react app. Every time i reference the data in the table cells in returns the same element for all the columns. I would like to display each of the dates in each column.
My Table:
function SomeComponenet(props) {
return (
<React.Fragment>
{props.attendence.map.forEach((attendence, index) => {
return (
<Paper>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">
{attendence.Attendence[index].date}
</TableCell>
<TableCell align="right">
{attendence.Attendence[index].date}
</TableCell>
<TableCell align="right">
{attendence.Attendence[index].date}
</TableCell>
<TableCell align="right">
{attendence.Attendence[index].date}
</TableCell>
<TableCell align="right">
{attendence.Attendence[index].date}
</TableCell>
</TableRow>
</TableHead>
</Table>
</Paper>
);
})}
</React.Fragment>
);
}
My data:
const fakeData = [
{
Name: "A Person",
Attendence: [
{
date: "2019/12/01",
attendence: 1
},
{
date: "2019/12/02",
attendence: 1
},
{
date: "2019/12/03",
attendence: 1
}
]
}
];
you need another map inside the {props.attendence.map}
link to codesandbox https://codesandbox.io/s/sad-grass-sg5hr
import React, { Fragment } from "react";
import { Table } from "reactstrap";
function Test() {
const attendence = [
{
Name: "A Person",
Attendence: [
{
date: "2019/12/01",
attendence: 1
},
{
date: "2019/12/02",
attendence: 1
},
{
date: "2019/12/03",
attendence: 1
}
]
}
];
return (
<Fragment>
{attendence.map(person => {
return (
<Table>
<thead>
<tr>
<th>Name</th>
{person.Attendence.map(personAttendendance => {
return <th>{personAttendendance.date}</th>;
})}
</tr>
</thead>
<tbody>
<tr>
<td>{person.Name}</td>
{person.Attendence.map(personAttendendance => {
return <td>{personAttendendance.attendence}</td>;
})}
</tr>
</tbody>
</Table>
);
})}
</Fragment>
);
}
export default Test;
that's because all the indexes are the same in one iteration of the topper map. you can't use forEach on a map this way.
you can remove forEach and do another map on attendence.Attendence
something like this:
function SomeComponenet(props) {
return (
<React.Fragment>
{props.attendence.map((attendence, index) =>{
{console.log(attendence.Attendence)}
return(
<Paper >
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
{attendence.Attendence.map( newAttendence => {
return(
<TableCell align="right">
{newAttendence.date}
</TableCell>
)
})}
</TableRow>
</TableHead>
</Table>
</Paper>
);
})}
</ReactFragment>
);
}

Refactoring large View component in React

I have a rather large view component to render a form where the user can add comments, upload images, select between multiple other products and submit it. I keep my state and functions in a separate component and am quite happy with that setup.
The view has grown quite substantially and I am not entirely happy with the structure of the app.
Particularly it annoys me that
I have to pass the same props and classes variables to each component I use
I have a mix of logic in my main view component CreateEntryForm and each of the smaller components
Any tips on how to better structure this code is much appreciated
import React from 'react';
import TextField from 'material-ui/TextField';
import Button from 'material-ui/Button';
import { MenuItem } from 'material-ui/Menu';
import { withStyles } from 'material-ui/styles';
import Chip from 'material-ui/Chip';
import Typography from 'material-ui/Typography';
import { CircularProgress } from 'material-ui/Progress';
import EntryImageView from "./entryImageView";
import {MarkedImage} from "./markedImage";
const styles = theme => ({
button: {
margin: theme.spacing.unit,
},
container: {
display: 'flex',
flexWrap: 'wrap',
},
menu: {
width: 200,
},
chipContainer: {
marginTop: theme.spacing.unit * 2,
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: theme.spacing.unit / 2,
},
wrapper: {
marginTop: theme.spacing.unit*2
},
});
const ProductFormGroup = (props) => (
<div>
<TextField
id="selectedProduct"
required
select
label="Select a product"
error={props.selectProductError !== ''}
margin="normal"
fullWidth
value={(props.currentEntry.currentProduct === undefined || props.currentEntry.currentProduct === null) ? "" : props.currentEntry.currentProduct}
onChange={props.handleInputChange('currentProduct')}
SelectProps={{
MenuProps: {
className: props.menu,
},
name: "currentProduct"
}}
helperText={props.selectProductError || ""}
>
{props.job.selectedProducts.map((product, index) => (
<MenuItem key={index} value={product}>
{product.name}
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
id="productQuantity"
label="Product Quantity"
fullWidth
name="productQuantity"
onChange={props.handleInputChange('productQuantity')}
value={props.productQuantity} />
<Button color="primary"
onClick={() => props.addSelectedChip()}
disabled={(props.productQuantity === '' || props.currentEntry.currentProduct === null)}>
add product
</Button>
</div>
);
const SelectedProductsGroup = (props) => (
<div>
<TextField
id="currentUpload"
select
label="Select an image to mark"
margin="normal"
fullWidth
value={props.currentUpload || ''}
onChange={props.handleInputChange('currentUpload')}
SelectProps={{
MenuProps: {
className: props.menu,
},
name: "currentUpload"
}}
>
{props.job.selectedUploads.map((file, index) => (
<MenuItem key={index} value={file}>
{file.name}
</MenuItem>
))}
</TextField>
{props.currentUpload &&
<Button color="primary" onClick={() => props.handleAttachmentDialogOpen(props.job.selectedUploads)}>
Edit marker on image
</Button>}
{props.selectedMarkedImage &&
<MarkedImage markerPosition={props.selectedMarkedImage.position}
attachment={props.selectedMarkedImage.attachment}
imageLoaded={props.markedImageLoaded}
handleImageLoaded={props.handleMarkedImageLoaded}
/>}
</div>
);
const SelectedProductsChipContainer = (props) => (
<div className={props.classes.wrapper}>
<Typography type="subheading" gutterBottom>
Selected Products
</Typography>
<div className={props.classes.chipContainer}>
{props.selectedProducts.map((product, index) => {
return (
<Chip
label={`${product.name} (${product.productQuantity})`}
key={index}
className={props.classes.chip}
onRequestDelete={() => props.handleRequestDeleteChip(product, "product")}
/>
)
})}
</div>
</div>
);
const SelectedImagesView = (props) => (
<div className={props.classes.wrapper}>
<Typography type="subheading" gutterBottom>
Selected images
</Typography>
<input type="file" id="myFile" onChange={props.handleFileUpload} />
{props.uploadLoading
? <CircularProgress/>
: null}
{props.selectedUploads.length > 0 && <EntryImageView selectedUploads={props.selectedUploads}
handleRequestDeleteChip={props.handleRequestDeleteChip} />}
</div>
);
const LocationDescriptionTextField = (props) => (
<TextField
id="locationDescription"
label="Location Description"
multiline
rows="4"
value={props.locationDescription}
onChange={props.handleInputChange('locationDescription')}
margin="normal"
fullWidth
/>
);
const CommentsTextField = (props) => (
<TextField
id="comments"
label="Comments"
multiline
rows="4"
value={props.comments}
onChange={props.handleInputChange('comments')}
margin="normal"
fullWidth
/>
);
export const CreateEntryForm = (props) => {
const { classes } = props;
return (
<div>
{props.job && <SelectedProductsGroup {...props} classes={classes}/>}
{props.selectedProducts.length > 0 && <SelectedProductsChipContainer {...props} classes={classes}/>}
{props.job && <ProductFormGroup {...props}/>}
<SelectedImagesView {...props} classes={classes} />
<LocationDescriptionTextField {...props} />
<CommentsTextField {...props} />
<Button color="primary" onClick={props.handleSubmit}>Create entry</Button>
</div>
)
};
export default withStyles(styles)(CreateEntryForm);

react-router+antD/ How to highlight a menu item when press back/forward button?

I create a menu and want to highlight the item which i choose,and i did it. But when i press back/forward button,the menu item don't highlight. What should i do?
I have tried to use addEventListener but failed.
Have someone could give some advice?
class Sidebar extends React.Component {
constructor(props) {
super(props);
this.state={
test: "home"
}
this.menuClickHandle = this.menuClickHandle.bind(this);
}
componentWillMount(){
hashHistory.listen((event)=>{
test1 = event.pathname.split("/");
});
this.setState({
test:test1[1]
});
}
menuClickHandle(item) {
this.props.clickItem(item.key);
}
onCollapseChange() {
this.props.toggle();
}
render() {
var {collapse} = this.props;
return (
<aside className="ant-layout-sider">
<Menu mode="inline" theme="dark" defaultSelectedKeys={[this.state.test || "home"]} onClick={this.menuClickHandle.bind(this)}>
<Menu.Item key="home">
<Link to="/home">
<Icon type="user"/><span className="nav-text">用户管理</span>
</Link>
</Menu.Item>
<Menu.Item key="banner">
<Link to="/banner">
<Icon type="setting"/><span className="nav-text">Banner管理</span>
</Link>
</Menu.Item>
</Menu>
<div className="ant-aside-action" onClick={this.onCollapseChange.bind(this)}>
{collapse ? <Icon type="right"/> : <Icon type="left"/>}
</div>
</aside>
)
}
}
I could come up with a solution using WithRouter
import React,{ Component } from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { Layout, Menu, Icon } from 'antd';
import PropTypes from 'prop-types';
const { Sider } = Layout;
class SideMenu extends Component{
static propTypes = {
location: PropTypes.object.isRequired
}
render() {
const { location } = this.props;
return (
<Sider
trigger={null}
collapsible
collapsed={this.props.collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['/']}
selectedKeys={[location.pathname]}>
<Menu.Item key="/">
<NavLink to="/">
<Icon type="home" />
<span>Home</span>
</NavLink>
</Menu.Item>
<Menu.Item key="/other">
<NavLink to="/other">
<Icon type="mobile"/>
<span>Applications</span>
</NavLink>
</Menu.Item>
<Menu.Item key="/notifications">
<NavLink to="/notifications">
<Icon type="notification" />
<span>Notifications</span>
</NavLink>
</Menu.Item>
</Menu>
</Sider>
)
}
}
export default withRouter(SideMenu);
Intercepts the current URL and then set selectedKeys(Note that it is not defaultSelectedKeys).
componentWillMount(){
hashHistory.listen((event)=>{
pathname = event.pathname.split("/");
if(pathname != null){
this.setState({
test:pathname[1]
});
}
});
}
you can set the paths of the link as keys on each Menu.Item . then selectedKeys={this.props.location.pathname}
<Menu
theme="light"
mode='inline'
selectedKeys={[this.props.location.pathname,]}
>
<Menu.Item key={item.path} style={{float:'right'}}>
Link to={item.path}>{item.name}</Link>
</Menu.Item>
{menulist}
</Menu>
Item would be set active according to the current path.
i added [] and trailing comma because selectedKeys accepts array while this.props.location.pathname is a String. i just code as hobby so idont know if its acceptable.
The following answer assumes you are using hooks. I know you are not in your question, but it might be useful for other people. In addition, this solution will work if you have nested paths such as /banner/this/is/nested, and it works not only when pressing back and forward buttons but also when refreshing the current page:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Layout, Menu } from 'antd'
const { Sider } = Layout
const items = [
{ key: '1', label: 'Invoices', path: '/admin/invoices' },
{ key: '2', label: 'Service Details', path: '/admin/service-details' },
{ key: '3', label: 'Service Contract Details', path: '/admin/service-contract-details' },
{ key: '4', label: 'Cost Centers', path: '/admin/cost-centers' },
{ key: '5', label: 'Clients', path: '/admin/clients' },
{ key: '6', label: 'Vendors', path: '/admin/vendors' }
]
const Sidebar = () => {
const location = useLocation()
const history = useHistory()
const [selectedKey, setSelectedKey] = useState(items.find(_item => location.pathname.startsWith(_item.path)).key)
const onClickMenu = (item) => {
const clicked = items.find(_item => _item.key === item.key)
history.push(clicked.path)
}
useEffect(() => {
setSelectedKey(items.find(_item => location.pathname.startsWith(_item.path)).key)
}, [location])
return (
<Sider style={{ backgroundColor: 'white' }}>
<h3 style={{ paddingLeft: '1rem', paddingTop: '1rem', fontSize: '1.25rem', fontWeight: 'bold', minHeight: 64, margin: 0 }}>
Costek
</h3>
<Menu selectedKeys={[selectedKey]} mode='inline' onClick={onClickMenu}>
{items.map((item) => (
<Menu.Item key={item.key}>{item.label}</Menu.Item>
))}
</Menu>
</Sider>
)
}
export default Sidebar
This is how the sidebar will look like:
#Nadun's solution works for paths that don't contains arguments. If you're however using arguments in your routes, like me, here's a solution that should work for any route path, including /users/:id or crazy stuff like /users/:id/whatever/:otherId. It uses react-router's matchPath API, which uses the exact same logic as the Router component.
// file with routes
export const ROUTE_KEYS = {
ROOT: "/",
USER_DETAIL: "/users/:id",
};
export const ROUTES = {
ROOT: {
component: Home,
exact: true,
key: ROUTE_KEYS.ROOT,
path: ROUTE_KEYS.ROOT,
},
USER_DETAIL: {
component: Users,
key: ROUTE_KEYS.USER_DETAIL,
path: ROUTE_KEYS.USER_DETAIL,
},
};
.
// place within the App component
<Router>
<Layout>
<MyMenu />
<Layout>
<Layout.Content>
{Object.values(ROUTES).map((route) => (
<Route {...route} />
))}
</Layout.Content>
</Layout>
</Layout>
</Router>
.
// MyMenu component
const getMatchedKey = (location) =>
(
Object.values(ROUTES).find((route) =>
matchPath(location.pathname, route)
) || {}
).path;
const MyMenu = ({ location }) => {
return (
<Layout.Sider>
<AntMenu mode="inline" selectedKeys={[getMatchedKey(location)]}>
<AntMenu.SubMenu
title={
<React.Fragment>
<Icon type="appstore" />
Home
</React.Fragment>
}
>
<AntMenu.Item key={ROUTE_KEYS.ROOT}>
<Icon type="appstore" />
<span className="nav-text">
Some subitem
</span>
</AntMenu.Item>
</AntMenu.SubMenu>
<AntMenu.SubMenu
title={
<React.Fragment>
<Icon type="user" />
Users
</React.Fragment>
}
>
<AntMenu.Item key={ROUTE_KEYS.USER_DETAIL}>
<Icon type="user" />
<span className="nav-text">
User detail
</span>
</AntMenu.Item>
</AntMenu.SubMenu>
</AntMenu>
</Layout.Sider>
);
};
export default withRouter(MyMenu);
I do something like this but it doesn't seem to be reactive. Like if I navigate to a new page through a button (not from the menu items), it will not update the active link until the page refreshes.
import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';
const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;
const { appName } = appConfig;
const AppNavigation = React.createClass({
getInitialState() {
return {
current: this.props.pathname
};
},
handleClick(e) {
browserHistory.push(e.key);
this.setState({ current: e.key });
return;
},
render() {
return (
<Row className='landing-menu' type="flex" justify="space-around" align="middle" style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
<Col span='19'>
<Link to='/'>
<h2 style={{fontSize: 21, color: '#fff'}}>
{appName}
<Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
</h2>
</Link>
</Col>
<Col span='5'>
<Menu onClick={this.handleClick} selectedKeys={[this.state.current]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
<Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
<Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
<Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
</Menu>
</Col>
</Row>
);
},
});
export const App = React.createClass({
propTypes: {
children: React.PropTypes.element.isRequired,
},
componentWillMount(){
if (Meteor.userId()) {
browserHistory.push('/student/home')
}
},
render() {
return (
<div style={{position: 'relative'}}>
<AppNavigation pathname={this.props.location.pathname} />
<div style={{minHeight: '100vh'}}>
{ this.props.children }
</div>
</div>
);
}
});
EDIT:
the below works pretty well. pass down the pathname from react-router and pop that as a prop into selectedKeys
import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';
const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;
const { appName } = appConfig;
const AppNavigation = React.createClass({
getInitialState() {
return {
current: this.props.pathname
};
},
handleClick(e) {
browserHistory.push(e.key);
this.setState({ current: e.key });
return;
},
render() {
return (
<Row className='landing-menu' type="flex" justify="space-around" align="middle" style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
<Col span='19'>
<Link to='/'>
<h2 style={{fontSize: 21, color: '#fff'}}>
{appName}
<Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
</h2>
</Link>
</Col>
<Col span='5'>
<Menu onClick={this.handleClick} selectedKeys={[this.props.pathname]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
<Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
<Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
<Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
</Menu>
</Col>
</Row>
);
},
});
export const App = React.createClass({
propTypes: {
children: React.PropTypes.element.isRequired,
},
componentWillMount(){
if (Meteor.userId()) {
browserHistory.push('/student/home')
}
},
render() {
return (
<div style={{position: 'relative'}}>
<AppNavigation pathname={this.props.location.pathname} />
<div style={{minHeight: '100vh'}}>
{ this.props.children }
</div>
</div>
);
}
});
If you are using an array and mapping over it (as in my case) to set menu Items, They must be in the same order as they appear in the Side menu otherwise, an active bar or background will not be shown.
Environment: React Router V5, Ant Design V4.17.0
I solved this issues by override the onClick props of Menu.Item of antd
<Menu theme="light" mode="inline">
{menuItems.map((item) => {
return (
<NavLink
to={item.navigation}
component={({ navigate, ...rest }) => <Menu.Item {...rest} onClick={navigate} />}
key={item.key}
activeClassName="ant-menu-item-selected"
>
{item.icons}
<span>{item.name}</span>
</NavLink>
)
}
)}
</Menu>
The NavLink component will pass navigate prop to Menu.Item, we need to map it to onClick prop and click behaviour will work correctly.