I'm trying to run some simple unit tests on React with Material UI but I'm getting an error that I don't know the cause of, I've ran tests with MaterialUI on other projects with no problems.
This is the code of the component I'm trying to test:
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import ReactRouterPropTypes from 'react-router-prop-types';
import { Icon } from 'react-fa';
import { withStyles } from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar';
import MaterialIcon from 'material-ui/Icon';
import IconButton from 'material-ui/IconButton';
import Modal from 'material-ui/Modal';
import Hidden from 'material-ui/Hidden';
import SimpleButton from '../../common/SimpleButton';
import OutlineButton from '../../common/OutlineButton';
import * as sessionActions from '../../../actions/sessionActions';
import { APP_TITLE, LOGOUT, LOGIN, REGISTER } from '../../../constants/strings';
import { CHECKOUT } from '../../../constants/routes';
import styles from './styles';
import CartIcon from '../../common/Icons/Cart.svg';
import logo from '../../../images/logo.png';
import logoXs from '../../../images/logoXS.png';
import Login from '../Login';
import Register from '../Register';
import Wrapper from '../../common/Wrapper';
class Header extends Component {
constructor(props) {
super(props);
this.state = {};
}
handleLogoutClick = () => {
this.props.actions.logout();
};
handleOpenModal = (s) => {
this.setState({ openModal: s });
};
handleCloseModal = () => {
this.setState({ openModal: null });
};
handleCartIcon = () => {
this.props.history.push(CHECKOUT);
};
renderAuthenticated = () => (
<SimpleButton onClick={this.handleLogoutClick} className={this.props.classes.button}>
{LOGOUT}
</SimpleButton>
);
renderNotAuthenticated = () => {
const { classes, history } = this.props;
return (
<Fragment>
<SimpleButton onClick={() => this.handleOpenModal(LOGIN)} className={classes.button} id="btn-login-modal">
<Hidden smUp>
<Icon className={classes.icons} name="user-circle" alt="login" size="2x" />
</Hidden>
<Hidden xsDown>{LOGIN}</Hidden>
</SimpleButton>
<Modal open={this.state.openModal === LOGIN} onClose={this.handleCloseModal}>
<Login
history={history}
handleCloseModal={this.handleCloseModal}
handleChangeModal={this.handleOpenModal}
/>
</Modal>
<Hidden xsDown>
<OutlineButton
onClick={() => this.handleOpenModal(REGISTER)}
color="primary"
className={classes.button}
>
{REGISTER}
</OutlineButton>
<Modal open={this.state.openModal === REGISTER} onClose={this.handleCloseModal}>
<Register
history={history}
handleCloseModal={this.handleCloseModal}
handleChangeModal={this.handleOpenModal}
/>
</Modal>
</Hidden>
</Fragment>
);
};
render() {
const { authenticated, classes } = this.props;
return (
<AppBar position="static" color="default" className={classes.appBar}>
<Wrapper>
<Toolbar className={classes.toolBar}>
<div className={classes.flex}>
<Hidden smUp>
<img src={logoXs} alt={APP_TITLE} />
</Hidden>
<Hidden xsDown>
<img src={logo} alt={APP_TITLE} />
</Hidden>
</div>
{authenticated ? this.renderAuthenticated() : this.renderNotAuthenticated()}
<IconButton className={classes.button} onClick={() => this.handleCartIcon()}>
<MaterialIcon>
<img src={CartIcon} alt="cart" />
</MaterialIcon>
</IconButton>
</Toolbar>
</Wrapper>
</AppBar>
);
}
}
const { objectOf, any, bool } = PropTypes;
Header.propTypes = {
/* eslint-disable react/no-typos */
classes: objectOf(any).isRequired,
actions: objectOf(any).isRequired,
authenticated: bool.isRequired,
history: ReactRouterPropTypes.history.isRequired,
};
const mapDispatch = dispatch => ({
actions: bindActionCreators(sessionActions, dispatch),
});
const mapStateToProps = ({ session }) => ({
authenticated: session.authenticated,
checked: session.checked,
});
export default connect(mapStateToProps, mapDispatch)(withStyles(styles)(Header));
This is the test file:
import React from 'react';
import { shallow } from 'enzyme';
import Header from './';
import configureStore from '../../../store/configureStore';
const store = configureStore();
const header = shallow(<Header store={store} />);
describe('<Header />', () => {
it('should open modal when clicking `SimpleButton`', () => {
console.log(header.dive().debug());
});
});
And in when I run the tests I get this:
FAIL src/components/containers/Header/Header.test.js ● <Header /> › should open modal when clicking `SimpleButton`
TypeError: Cannot read property '200' of undefined
6 | padding: '10px 5px',
7 | boxShadow: `-webkit-box-shadow: 0px 4px 10px 0px ${
> 8 | theme.palette.gray[200]
9 | }; -moz-box-shadow: 0px 4px 10px 0px ${theme.palette.gray[200]}; box-shadow: 0px 4px 10px 0px ${
10 | theme.palette.gray[200]
11 | }`,
at styles (src/components/containers/Header/styles.js:8:7)
at Object.create (node_modules/material-ui/styles/getStylesCreator.js:31:35)
at WithStyles.attach (node_modules/material-ui/styles/withStyles.js:275:45)
at WithStyles.componentWillMount (node_modules/material-ui/styles/withStyles.js:205:16)
at ReactShallowRenderer._mountClassComponent (node_modules/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:137:22)
at ReactShallowRenderer.render (node_modules/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:102:14)
at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:287:35
at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:94:16)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:286:68)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:119:22)
at ShallowWrapper.wrap (node_modules/enzyme/build/ShallowWrapper.js:1648:16)
at ShallowWrapper.<anonymous> (node_modules/enzyme/build/ShallowWrapper.js:1718:26)
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1620:25)
at ShallowWrapper.dive (node_modules/enzyme/build/ShallowWrapper.js:1710:21)
at Object.<anonymous> (src/components/containers/Header/Header.test.js:12:24)
I've tried using mount, wrapping the component in MuiThemeProvider, using the helper createShallow or createMount from MaterialUI.
Nothing works.
AppBar requires a muiTheme context, which you can provide to .dive() (see documentation)
Here is my test for a Header component rendering an AppBar material-ui component.
import React from "react";
import {shallow} from "enzyme";
import Header from "../";
describe("Header", () => {
it("should match the snapshot", () => {
const wrapper = shallow(<Header />);
// dive in AppBar
const wrappedComponents = wrapper.dive({
context: {
muiTheme: {
appBar: "",
zIndex: 0,
prepareStyles: () => {},
button: {
iconButtonSize: 0,
},
},
},
});
expect(wrappedComponents).toMatchSnapshot();
});
});
Related
I have this problem upgrading my react application. Basically, I followed the damn mui-v5 documentation, and I still have some erros in my app.tsx file.
import { useReactOidc, withOidcSecure } from '#axa-fr/react-oidc-context';
import { Container } from '#mui/material';
import CssBaseline from '#mui/material/CssBaseline';
import {
ThemeProvider,
StyledEngineProvider,
Theme,
} from '#mui/material/styles';
import createStyles from '#mui/styles/createStyles';
import makeStyles from '#mui/styles/makeStyles';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Redirect, Route, Switch } from 'react-router-dom';
import { RootState } from 'src/lib/configuration/store-configuration';
import { DarkTheme, LightTheme } from 'src/components/material-ui-theme';
import routes from 'src/components/routes/routes';
import BreadCrumbs from 'src/components/shared/breadcrumbs/breadcrumbs';
import ErrorBoundary from 'src/components/shared/error/Error';
import Footer from 'src/components/shared/footer/footer';
import Header from 'src/components/shared/header/header';
import Notifier from 'src/components/shared/notifications';
import ScrollTop from 'src/components/shared/scroll-top/scroll-top';
import Sider from 'src/components/shared/sider';
import { useTranslation } from 'react-i18next';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(1),
},
}),
);
const App = () => {
const classes = useStyles();
const { oidcUser } = useReactOidc();
const appStore = useSelector((store: RootState) => store.app);
const [drawerOpen, setDrawerOpen] = useState(false);
const handleDrawerToggle = () => {
setDrawerOpen(!drawerOpen);
};
const { i18n } = useTranslation();
useEffect(() => {
document.documentElement.lang = i18n.language;
}, [i18n.language]);
return (
<StyledEngineProvider injectFirst>
<ThemeProvider
theme={
appStore.theme === 'dark' ? DarkTheme : LightTheme
}
>
<div className={classes.root}>
<CssBaseline />
<Header handleDrawerToggle={handleDrawerToggle} />
{oidcUser ? (
<Sider
drawerOpen={drawerOpen}
handleDrawerToggle={
handleDrawerToggle
}
/>
) : null}
<main className={classes.content}>
<div id="back-to-top-anchor" />
<div className={classes.toolbar} />
<Container
maxWidth="xl"
className={classes.container}
>
<Notifier />
<BreadCrumbs />
<ErrorBoundary>
<Switch>
{routes.map(
(route, i) => (
<Route
key={
'route_' +
i
}
exact={
route.exact
}
path={
route.path
}
component={
route.secure
? withOidcSecure(
route.component,
)
: route.component
}
/>
),
)}
<Redirect to="/" />
</Switch>
</ErrorBoundary>
</Container>
<ScrollTop />
<Footer />
</main>
</div>
</ThemeProvider>
</StyledEngineProvider>
);
};
export default App;
Here is the navigator console error :
I tried to modify some imports and used:
import { createTheme } from '#material-ui/core';
const theme = createTheme();
And passing it to makestyles((theme) => ... but nothing changed.
Please guys I need your support ! :)
link for the open-source github project: https://github.com/InseeFr/sugoi-ui
I am trying to test a component which use useTheme hook provided by emotion.js. The theme is set during the app initialization.
Now when I write my test cases, the useTheme hook is unable to fetch the styles data as only header component is getting mounted for testing. How to mock the data provided by hooks.
app.js
import { theme } from '../src/utils/styles/themes/default';
const AppRoot = ({ path, Router }) => {
const routeRenderFunction = (props) => <RouteHandler route={props} />;
return (
<ThemeProvider theme={theme}>
<Router location={path} context={{}}>
<Switch>
{routePatterns.map((routePattern) => (
<Route key={routePattern} path={routePattern} render={routeRenderFunction} />
))}
</Switch>
</Router>
</ThemeProvider>
);
};
header.js
import React from "react";
import { css } from "emotion";
import { useTheme } from "emotion-theming";
import * as styles from "./Header.style";
const Header = ({userName = 'Becky Parsons', clinicName = 'The University of Southampton'}) => {
const theme = useTheme();
return (
<div className={css(styles.accountDetails(theme))}>
<div className={css(styles.accountContainer)}>
<div className={css(styles.accountSpecifics)}>
<h5 className={css(styles.accountDetailsH5(theme))} data-testid="user-name">{userName}</h5>
<h6 className={css(styles.accountDetailsH6(theme))} data-testid="clinic-name">
{clinicName}
</h6>
</div>
<div className={css(styles.avatar)} />
</div>
</div>
);
};
export default Header;
header.test.js
import React from 'react'
import {render} from '#testing-library/react';
import Header from './Header';
test('Check if header component loads', () => {
const { getByTestId } = render(<Header userName='Becky Parsons' clinicName='The University of Southampton'/>);
expect(getByTestId('user-name').textContent).toBe('Becky Parsons');
expect(getByTestId('clinic-name').textContent).toBe('The University of Southampton');
})
header.style.js
export const accountDetails = theme => ({
height: '70px',
backgroundColor: theme.header.backgroundColor,
textAlign: 'right',
padding: '10px 40px'
});
export const accountContainer = {
display: 'flex',
justifyContent: 'flex-end'
};
Error: Uncaught [TypeError: Cannot read property 'backgroundColor' of undefined]
You should wrap your component with ThemeProvider, try this;
test('Check if header component loads', () => {
const { getByText } = render(
<ThemeProvider theme={your theme object...}>
<Header userName='Becky Parsons' clinicName='The University of Southampton'/>
</ThemeProvider >
);
...
});
and you can look here: https://github.com/emotion-js/emotion/blob/master/packages/emotion-theming/tests/use-theme.js
I am converting the React + Material UI + Firebase project found on the Material UI doc's page. I am trying to open the SignUp Dialog (i.e. SignUp Modal). Here is a simplified version of the 4 corresponding files and I need help opening up the sign up modal.
App.js
import React, { useState } from 'react';
import { ThemeProvider } from '#material-ui/core/styles';
import Navbar from './components/Navbar'
import DialogHost from './components/DialogHost';
import Loading from './components/Loading'
import theme from './theme';
function App() {
const [signedIn] = useState(false)
const [ready] = useState(true) //!toggle for testing
const [dialog, setDialog] = useState({
isOpenSignUp: false,
isOpenSignIn: false
});
const openDialog = e => {
setDialog({...dialog,[e.target.name]: true});
}
const closeDialog = e => {
setDialog({...dialog,[e.target.name]: false});
}
return (
<ThemeProvider theme={theme}>
{!ready &&
<Loading />
}
{ready &&
<>
<Navbar
signedIn={signedIn}
onSignUpClick={openDialog}
onSignInClick={openDialog}
/>
<DialogHost
signedIn={signedIn}
dialogs={
{
signUpDialog: {
dialogProps: {
open: dialog.isOpenSignUp,
//onClose below hasn't been converted... not entirely sure how
onClose: (callback) => {
this.closeDialog('signUpDialog');
if (callback && typeof callback === 'function') {
callback();
}
}
}
},
}
}
/>
</>
}
</ThemeProvider>
);
}
export default App;
Navbar.js
import React from 'react'
import PropTypes from 'prop-types';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import Box from '#material-ui/core/Box';
import Button from '#material-ui/core/Button';
const Navbar = ({ signedIn, onSignUpClick, onSignInClick }) => {
return (
<AppBar color="primary" position="static">
<Toolbar variant="regular">
<Box flexGrow={1}>
<Typography color="inherit" variant="h6">{process.env.REACT_APP_NAME}</Typography>
</Box>
{!signedIn &&
<>
<Box mr={1}>
<Button name="isOpenSignUp" color="secondary" variant="contained" onClick={onSignUpClick}>Sign Up</Button> //GAVE THE BUTTON NAMES, BUT THIS DOESN'T SEEM CORRECT...
</Box>
<Button name="isOpenSignIn" color="secondary" variant="contained" onClick={onSignInClick}>Sign In</Button>
</>
}
</Toolbar>
</AppBar>
)
}
Navbar.defaultProps = {
signedIn: false
};
Navbar.propTypes = {
signedIn: PropTypes.bool.isRequired
};
export default Navbar
DialogHost.js
import React from 'react'
import PropTypes from 'prop-types';
import Hidden from '#material-ui/core/Hidden';
import SignUpDialog from '../../pages/SignUpDialog';
const DialogHost = ({ signedIn, dialogs }) => {
const signUpDialog = dialogs.signUpDialog;
return (
<>
<Hidden xsDown>
{!signedIn &&
<>
<SignUpDialog
dialogProps={signUpDialog.dialogProps}
{...signUpDialog.props}
/>
</>
}
</Hidden>
<Hidden smUp>
{!signedIn &&
<>
<SignUpDialog
dialogProps={{
fullScreen: true,
...signUpDialog.dialogProps
}}
{...signUpDialog.props}
/>
</>
}
</Hidden>
</>
)
}
DialogHost.defaultProps = {
signedIn: false
};
DialogHost.propTypes = {
signedIn: PropTypes.bool.isRequired,
dialogs: PropTypes.object.isRequired
};
export default DialogHost
I also have a SignUpDialog.js file, but all it is is rendering a functional component with a title of 'Sign Up'
After deployment of my app on Heroku, it crashes with the message InternalError: "too much recursion"then Invariant Violation: "Minified React error #31; visit https://reactjs.org/docs/error-decoder.html?invariant=31&args[]=InternalError%3A%20too%20much%20recursion&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.... when I try to access the home page.The page renders nothing also. The other pages seem to work fine but I have to edit the URL in the browzer to access them. I don't see what I am doing wrong.
If I try keeping track on how many times the fetch in componentDidMount is ran, the error disappears. I don't see a problem with the fumction beeing called also. I checked for the trailing JS semi-columns after tags and made sure that all components return a valid jsx.
Home/index.js
import React, { Component } from "react";
import PageWrapper from "../PageWrapper";
import { connect } from "react-redux";
import { fetchHighlightedCourses } from "../../store/actions";
import AsyncWrapper from "../AsyncWrapper";
const AsyncCourseList = AsyncWrapper(() => import("./CourseList"));
class Home extends Component {
componentDidMount() {
const { highlightedCourses } = this.props;
if (!Array.isArray(highlightedCourses) || highlightedCourses.length === 0)
this.props.fetchHighlightedCourses();
}
_renderCourseList = () => {
const { highlightedCourses } = this.props;
console.log(highlightedCourses);
if (Array.isArray(highlightedCourses) && highlightedCourses.length > 0)
return <AsyncCourseList courses={highlightedCourses} />;
return <div> </div>
};
render() {
return (
<PageWrapper title="Acceuil">
{this._renderCourseList()}
</PageWrapper>
);
}
}
const mapStateToProps = state => {
return {
highlightedCourses: state.explore.highlightedCourses
};
};
export default connect(
mapStateToProps,
{ fetchHighlightedCourses }
)(Home);
Home/CourseList.js
import React from "react";
import classNames from "classnames";
import Grid from "#material-ui/core/Grid";
import Typography from "#material-ui/core/Typography";
import { withStyles } from "#material-ui/core/styles";
import { Link as RouterLink } from "react-router-dom";
import Link from "#material-ui/core/Link";
import CourseCard from "./CourseCard";
const styles = theme => ({
layout: {
width: "auto",
marginLeft: theme.spacing.unit * 3,
marginRight: theme.spacing.unit * 3
},
cardGrid: {
padding: `${theme.spacing.unit * 4}px 0`
},
catalogTitle: {
color: theme.palette.text.secondary,
marginBottom: theme.spacing.unit * 3
},
catalogLink: {
marginTop: theme.spacing.unit * 3
}
});
const CourseList = ({ classes, courses }) => {
if (Array.isArray(courses))
return (
<div className={classNames(classes.layout, classes.cardGrid)}>
<Typography variant="h6" className={classes.catalogTitle}>
{"Que voulez-vous apprendre aujourd'hui?"}
</Typography>
<Grid container spacing={24}>
{courses.map((course, index) => (
<Grid item key={index}>
<CourseCard course={course} />
</Grid>
))}
</Grid>
<Typography variant="h6" className={classes.catalogLink}>
<Link component={RouterLink} to={`/explore/classes`} underline="none">
{"Voir plus des Cours >"}
</Link>
</Typography>
</div>
);
return <div> </div>
};
export default withStyles(styles)(CourseList);
Home/CourseCard.js
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import Card from "#material-ui/core/Card";
import CardActionArea from "#material-ui/core/CardActionArea";
import CardActions from "#material-ui/core/CardActions";
import MoreHoriz from "#material-ui/icons/MoreHoriz";
import CardContent from "#material-ui/core/CardContent";
import Grow from "#material-ui/core/Grow";
import CardMedia from "#material-ui/core/CardMedia";
import Typography from "#material-ui/core/Typography";
import IconButton from "#material-ui/core/IconButton";
import DefaultImage from "./default.jpg";
import Menu from "#material-ui/core/Menu";
import MenuItem from "#material-ui/core/MenuItem";
import { Link as RouterLink } from "react-router-dom";
import Link from "#material-ui/core/Link";
const styles = {
card: {
width: 190,
height: "100%",
display: "flex",
flexDirection: "column"
},
media: {
height: 90
},
courseTitle: {
height: 65
//overflowX: 'auto'
},
cardContent: {
flexGrow: 1
}
};
class CourseCard extends Component {
constructor(props) {
super(props);
this.state = {
anchorEl: null
};
}
handleMoreClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleMoreClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { classes, course } = this.props;
const { anchorEl } = this.state;
return (
<Grow in timeout={300}>
<Card className={classes.card}>
<CardActionArea>
<Link
component={RouterLink}
to={`/explore/classe/${course._id}`}
underline="none"
>
<CardMedia
className={classes.media}
image={course.img || DefaultImage}
title="Default image"
/>
<CardContent className={classes.cardContent}>
<Typography
gutterBottom
variant="h6"
component="h6"
className={classes.courseTitle}
>
{course.title}
</Typography>
</CardContent>
</Link>
</CardActionArea>
<CardActions>
<IconButton
color="primary"
aria-label="more_options"
onClick={this.handleMoreClick}
>
<MoreHoriz />
</IconButton>
<Menu
id="more_options"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleMoreClose}
>
<MenuItem onClick={this.handleMoreClose}>S'inscrire</MenuItem>
<MenuItem onClick={this.handleMoreClose}>Détails</MenuItem>
</Menu>
</CardActions>
</Card>
</Grow>
);
}
}
CourseCard.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(CourseCard);
The following fixes the issue but I would like to know why.
class Home extends Component {
state = { fetched: 0 };
componentDidMount() {
const { highlightedCourses } = this.props;
if (
(!Array.isArray(highlightedCourses) || highlightedCourses.length === 0) &&
this.state.fetched < 3
) {
this.setState(prev => ({ fetched: prev.fetched + 1 }));
this.props.fetchHighlightedCourses();
}
}
...//the rest is the same
}
Here is the action
export const fetchHighlightedCourses = () => async dispatch => {
try {
dispatch({
type: HIGHLIGHTED_COURSE_LOADING
});
const res = await axios.get(highlightedCourseURL);
if (res.status === 200 && res.data.success) {
return await dispatch({
type: HIGHLIGHTED_COURSE_SUCCESS,
payload: res.data.content
});
}
return await dispatch({
type: HIGHLIGHTED_COURSE_FAILLURE,
payload: res.data.message
});
} catch (error) {
let message;
if (error.response && error.response.data && error.response.data.message)
message = error.response.data.message;
else message = "Erreur survenue lors du chargement de vos contacts";
return await dispatch({
type: HIGHLIGHTED_COURSE_FAILLURE,
payload: message
});
} finally {
return await dispatch({
type: HIGHLIGHTED_COURSE_DONE
});
}
};
Thank you
I need to implement a <BackButton /> in react-admin for example when I open show page for a resource I need able to back to the list page.
Can you guide me to implement this?
I'm not familiar with react-admin routing mechanism.
Now I'm using this component in my edit form actions props:
const MyActions = ({ basePath, data, resource }) => (
<CardActions>
<ShowButton basePath={basePath} record={data} />
<CloneButton basePath={basePath} record={data} />
{/* Need <BackButton /> here */}
</CardActions>
);
export const BookEdit = (props) => (
<Edit actions={<MyActions />} {...props}>
<SimpleForm>
...
</SimpleForm>
</Edit>
);
You can use react-router-redux's goBack() function to achieve this.
For example, your button component will look something like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Button from '#material-ui/core/Button';
import { goBack } from 'react-router-redux';
class BackButton extends Component {
handleClick = () => {
this.props.goBack();
};
render() {
return <Button variant="contained" color="primary" onClick={this.handleClick}>Go Back</Button>;
}
}
export default connect(null, {
goBack,
})(BackButton);
Now use that button component in your CardActions.
You can get help from an example which uses react-router-redux's push() function in a similar way from the official docs.
Create a back button. This one will pass props and children (text) and uses react-router directly, which I think makes more sense and keeps your code simple.
// BackButton.js
import React from 'react'
import Button from '#material-ui/core/Button'
import { withRouter } from 'react-router'
const BackButton = ({ history: { goBack }, children, ...props }) => (
<Button {...props} onClick={goBack}>
{children}
</Button>
)
export default withRouter(BackButton)
Example usage:
import { Toolbar, SaveButton } from 'react-admin'
import BackButton from './BackButton'
const SomeToolbar = props => (
<Toolbar {...props}>
<SaveButton />
<BackButton
variant='outlined'
color='secondary'
style={{ marginLeft: '1rem' }}
>
Cancel
</BackButton>
</Toolbar>
)
The complete code is below.
//BackButton.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import compose from 'recompose/compose';
import { withStyles, createStyles } from '#material-ui/core/styles';
import { translate } from 'ra-core';
import Button from '#material-ui/core/Button';
import ArrowBack from '#material-ui/icons/ArrowBack';
import classnames from 'classnames';
import { fade } from '#material-ui/core/styles/colorManipulator';
const styles = theme =>
createStyles({
deleteButton: {
color: theme.palette.error.main,
'&:hover': {
backgroundColor: fade(theme.palette.error.main, 0.12),
// Reset on mouse devices
'#media (hover: none)': {
backgroundColor: 'transparent',
},
},
},
});
const sanitizeRestProps = ({
basePath,
className,
classes,
label,
invalid,
variant,
translate,
handleSubmit,
handleSubmitWithRedirect,
submitOnEnter,
record,
redirect,
resource,
locale,
...rest
}) => rest;
class BackButton extends Component {
static contextTypes = {
router: () => true, // replace with PropTypes.object if you use them
}
static propTypes = {
label: PropTypes.string,
refreshView: PropTypes.func.isRequired,
icon: PropTypes.element,
};
static defaultProps = {
label: 'ra.action.back',
icon: <ArrowBack />,
};
render() {
const {
className,
classes = {},
invalid,
label = 'ra.action.back',
pristine,
redirect,
saving,
submitOnEnter,
translate,
icon,
onClick,
...rest
} = this.props;
return (
<Button
onClick={this.context.router.history.goBack}
label={label}
className={classnames(
'ra-back-button',
classes.backButton,
className
)}
key="button"
{...sanitizeRestProps(rest)}>
{icon} {label && translate(label, { _: label })}
</Button>
)
}
}
const enhance = compose(
withStyles(styles),
translate
);
export default enhance(BackButton);
//Toolbar.js
import React from 'react';
import {
Toolbar,
SaveButton,
DeleteButton,
} from 'react-admin';
import { withStyles } from '#material-ui/core';
import BackButton from './BackButton'
const toolbarStyles = {
toolbar: {
display: 'flex',
justifyContent: 'space-between',
},
};
export const CustomEditToolbar = withStyles(toolbarStyles)(props => (
<Toolbar {...props}>
<SaveButton/>
<DeleteButton/>
<BackButton/>
</Toolbar>
));
export const CustomCreateToolbar = withStyles(toolbarStyles)(props => (
<Toolbar {...props}>
<SaveButton/>
<BackButton/>
</Toolbar>
));