React-Admin I Custom button not able to get form data - reactjs

I have created a custom button to reject a user. I want to send the reason for rejecting but not able to send the form data.I am using "FormWithRedirect" in userEdit component.
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import ThumbDown from '#material-ui/icons/ThumbDown';
import { useUpdate, useNotify, useRedirect } from 'react-admin';
/**
* This custom button demonstrate using a custom action to update data
*/
const useStyles = makeStyles((theme) => ({
button: {
color: 'red',
borderColor: 'red',
"&:hover": {
color: '#fff',
background: 'red',
borderColor: 'red',
}
},
}));
const RejectButton = ({ record }: any) => {
const notify = useNotify();
const redirectTo = useRedirect();
const classes = useStyles();
const [reject, { loading }] = useUpdate(
`users/${record.id}/_reject`,
'',
{ reason: 'Due to missinng information or may be the license picture is not visible. Please upload again.' },
record,
{
undoable: true,
onSuccess: () => {
notify(
'User profile rejected!',
'info',
{},
true
);
redirectTo('/users');
},
onFailure: (error: any) => notify(`Error: ${error.error}`, 'warning'),
}
);
return (
<Button
variant="outlined"
color="primary"
size="small"
onClick={reject}
disabled={loading}
className={classes.button}
startIcon={<ThumbDown />}
>
Reject
</Button>
);
};
RejectButton.propTypes = {
record: PropTypes.object,
};
export default RejectButton;
this is my code for rejectButton component and in reason I am sending the static value but I want to send whatever I typed in TextArea component.

Related

How can I change my data fetching strategy to avoid stale data on initial dynamic route renders?

I'm building a Next.js app which allows users to create and view multiple Kanban boards. There are a couple of different ways that a user can view their different boards:
On the Home page of the app, users see a list of boards that they can click on.
The main navigation menu has a list of boards users can click on.
Both use Next.js Link components.
Clicking the links loads the following dynamic page: src/pages/board/[boardId].js The [boardId].js page fetches the board data using getServerSideProps(). An effect fires on route changes, which updates the redux store. Finally, the Board component uses a useSelector() hook to pull the data out of Redux and render it.
The problem I'm experiencing is that if I click back and forth between different boards, I see a brief flash of the previous board's data before the current board data loads. I am hoping someone can suggest a change I could make to my approach to alleviate this issue.
Source code:
// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'
const BoardPage = ({ board, tasks }) => {
const router = useRouter()
const dispatch = useDispatch()
useEffect(() => {
dispatch(hydrateTasks({ board, tasks }))
}, [router])
return (
<Board />
)
}
export async function getServerSideProps ({ query, req, res }) {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
const { boardId } = query
const boardQuery = prisma.board.findUnique({
where: {
id: boardId
},
select: {
name: true,
description: true,
id: true,
TO_DO: true,
IN_PROGRESS: true,
DONE: true
}
})
const taskQuery = prisma.task.findMany({
where: {
board: boardId
},
select: {
id: true,
title: true,
description: true,
status: true,
board: true
}
})
try {
const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
return { props: { board, tasks } }
} catch (error) {
console.log(error)
return { props: { board: {}, tasks: [] } }
}
}
export default BoardPage
// src/Components/Board/Board.js
import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch, useSelector } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'
const Board = () => {
const [css, theme] = useStyletron()
const dispatch = useDispatch()
useEffect(() => {
document.querySelector('body').style.background = theme.colors.backgroundPrimary
}, [theme])
// get data from Redux
const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)
// set up a boolean and a trigger to control "done"" animation
const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })
const doneTrigger = doneUseTrigger({ duration: 4000 })
// get width and height for confetti animation
const { width, height } = useWindowSize()
// curry the drag end handler for the drag and drop UI
const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })
return (
<ErrorBoundary>
<DragDropContext onDragEnd={curriedDragEnd}>
<div className={css({
marginLeft: '46px',
marginTop: '16px',
fontFamily: 'Roboto',
display: 'flex',
alignItems: 'baseline'
})}>
<h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
{boardDescription &&
<p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
}
</div>
<StyledBoardMain>
{columnOrder.map(columnKey => {
const column = columns[columnKey]
const tasksArray = column.taskIds.map(taskId => tasks[taskId])
return (
<Column
column={columnKey}
key={`COLUMN_${columnKey}`}
tasks={tasksArray}
title={column.title}
status={column.status}
/>
)
})}
</StyledBoardMain>
</DragDropContext>
{showDone && <Confetti
width={width}
height={height}
/>}
</ErrorBoundary>
)
}
export default Board
// src/pages/index.tsx
import React, {PropsWithChildren} from 'react'
import {useSelector} from "react-redux";
import {authOptions} from 'pages/api/auth/[...nextauth]'
import {unstable_getServerSession} from "next-auth/next"
import CreateBoardModal from 'Components/Modals/CreateBoard/CreateBoard'
import Link from 'next/link'
import {useStyletron} from "baseui";
const Index: React.FC = (props: PropsWithChildren<any>) => {
const {board: boards} = useSelector(state => state)
const [css, theme] = useStyletron()
return boards ? (
<>
<div style={{marginLeft: '46px', fontFamily: 'Roboto', width: '600px'}}>
<h1 className={css({fontSize: '22px'})}>Boards</h1>
{boards.map(({name, description, id}) => (
<Link href="/board/[boardId]" as={`/board/${id}`} key={id}>
<div className={css({
padding: '20px',
marginBottom: '20px',
borderRadius: '6px',
background: theme.colors.postItYellow,
cursor: 'pointer'
})}>
<h2 className={css({fontSize: '20px'})}>
<a className={css({color: theme.colors.primary, width: '100%', display: 'block'})}>
{name}
</a>
</h2>
<p>{description}</p>
</div>
</Link>
))}
</div>
</>
) : (
<>
<h1>Let's get started</h1>
<button>Create a board</button>
</>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(context.req, context.res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
return {props: {session}}
}
export default Index
It looks like there's only ever one board in the redux. You could instead use a namespace so that you don't have to keep swapping different data in and out of the store.
type StoreSlice = {
[boardId: string]: Board;
}
Then the "brief flash" that you will see will either be the previous data for the correct board, or nothing if it has not yet been fetched.

How to create forms with MUI (material ui)

Hiho,
I want to use mui in my current react project. Is there a different/better way to create forms than the following example?:
const [companyName, setCompanyName] = useState<string>("");
const [companyNameError, setCompanyNameError] = useState<boolean>(false);
const changeName = (event: React.ChangeEvent<HTMLInputElement>) => {
if(event.target.value === "") {
setCompanyNameError(true);
} else {
setCompanyNameError(false);
}
event.preventDefault();
setCompanyName(event.target.value);
}
const anyInputFieldEmpty = () => {
var result = false;
if(companyName === "") {
setCompanyNameError(true);
result = true;
} else {
setCompanyNameError(false);
}
// add the same check for all other fields. My real code has multiple input fields
return result;
}
const resetFields = () => {
setCompanyName("");
}
return (
<div>
<TextField
required
fullWidth
label="Company Name"
margin="dense"
name="companyName"
value={companyName}
onChange={changeName}
helperText={companyNameError ? "Company name is not allowed to be empty!" : ""}
error={companyNameError}
/>
<Button
sx={{ alignSelf: 'center', }}
variant="contained"
onClick={() => {
if(!anyInputFieldEmpty()) {
onSubmitClick(); // some function from somewhere else, which triggers logic
resetFields(); // This form is in a popover. The values should be resetted before the user open it again.
}
}}
>
Create
</Button>
</div>);
It feels wrong to do the validation this way if I use multiple textfields (up to 9). Its a lot of boilerplate code and if I add further validation rules (for example a minimum character count) it goes crazy.
Is this the right way to achive my goal?
T
As others have mentioned. Formik and Yup works great for validation. Formik also provides a way to easily disable your submit buttons. Here is a codesandbox : https://codesandbox.io/s/long-butterfly-seogsw?file=/src/App.js
I think you should check the Formik for hook-based validation and jsonforms, react-declarative from json-schema based view creation
Less code solutions is better on production, but for a learning reason it better to write real code based on hooks, contexts or redux reducers
import { useState } from "react";
import { One, FieldType } from "react-declarative";
const fields = [
{
type: FieldType.Text,
title: "First name",
defaultValue: "Peter",
name: "firstName"
},
{
type: FieldType.Text,
title: "Age",
isInvalid: ({ age }) => {
if (age.length === 0) {
return "Please type your age";
} else if (parseInt(age) === 0) {
return "Age must be greater than zero";
}
},
inputFormatterTemplate: "000",
inputFormatterAllowed: /^[0-9]/,
name: "age"
}
];
export const App = () => {
const [data, setData] = useState(null);
const handleChange = (data) => setData(data);
return (
<>
<One fields={fields} onChange={handleChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
);
};
export default App;
An example project could be found on this codesandbox
MUI does not have a native form validator
i recommend using react-hook-form + yup it's pretty simple and has a lot of tutorials
https://react-hook-form.com/get-started
EXEMPLE
TextFieldComponent
import { OutlinedTextFieldProps } from '#mui/material';
import React from 'react';
import { Control, useController } from 'react-hook-form';
import { InputContainer, TextFieldStyled } from './text-field.styles';
export interface TextFieldProps extends OutlinedTextFieldProps {
control: Control;
helperText: string;
name: string;
defaultValue?: string;
error?: boolean;
}
export const TextField: React.FC<TextFieldProps> = ({
control,
helperText,
name,
defaultValue,
error,
...rest
}) => {
const { field } = useController({
name,
control,
defaultValue: defaultValue || ''
});
return (
<InputContainer>
<TextFieldStyled
helperText={helperText}
name={field.name}
value={field.value}
onChange={field.onChange}
fullWidth
error={error}
{...rest}
/>
</InputContainer>
);
};
Styles
import { TextField } from '#mui/material';
import { styled } from '#mui/material/styles';
export const TextFieldStyled = styled(TextField)`
.MuiOutlinedInput-root {
background: ${({ theme }) => theme.palette.background.paper};
}
.MuiInputLabel-root {
color: ${({ theme }) => theme.palette.text.primary};
}
`;
export const InputContainer = styled('div')`
display: grid;
grid-template-columns: 1fr;
align-items: center;
justify-content: center;
width: 100%;
`;
export const InputIconStyled = styled('i')`
text-align: center;
`;
Usage
// Validator
const validator = yup.object().shape({
email: yup
.string()
.required(translate('contact.email.modal.email.required'))
.email(translate('contact.email.modal.email.invalid')),
});
// HookForm
const {
control,
handleSubmit,
formState: { errors }
} = useForm({
resolver: yupResolver(validator)
});
// Compoenent
<TextField
label="label"
fullWidth
placeholder="placeholder
size="small"
control={control}
helperText={errors?.name?.message}
error={!!errors?.name}
name={'name'}
variant={'outlined'}
/>

React Hook Form Register Different Forms With Same Field Names

I have a material ui stepper in which there are multiple forms using react-hook-form. When I change between steps, I would expect a new useForm hook to create new register functions that would register new form fields, but when I switch between steps, the second form has the data from the first. I've seen at least one question where someone was trying to create two forms on the same page within the same component but in this case I am trying to create two unique forms within different steps using different instances of a component. It seems like react-hook-form is somehow not updating the useForm hook or is recycling the form fields added to the first register call.
Why isn't react-hook-form using a new register function to register form fields to a new useForm hook? Or at least, why isn't a new useForm hook being created between steps?
DynamicForm component. There are two of these components (one for each step in the stepper).
import { Button, Grid } from "#material-ui/core";
import React from "react";
import { useForm } from "react-hook-form";
import { buttonStyles } from "../../../styles/buttonStyles";
import AppCard from "../../shared/AppCard";
import { componentsMap } from "../../shared/form";
export const DynamicForm = (props) => {
const buttonClasses = buttonStyles();
const { defaultValues = {} } = props;
const { handleSubmit, register } = useForm({ defaultValues });
const onSubmit = (userData) => {
props.handleSubmit(userData);
};
return (
<form
id={props.formName}
name={props.formName}
onSubmit={handleSubmit((data) => onSubmit(data))}
>
<AppCard
headline={props.headline}
actionButton={
props.actionButtonText && (
<Button className={buttonClasses.outlined} type="submit">
{props.actionButtonText}
</Button>
)
}
>
<Grid container spacing={2}>
{props.formFields.map((config) => {
const FormComponent = componentsMap.get(config.component);
return (
<Grid key={`form-field-${config.config.name}`} item xs={12}>
<FormComponent {...config.config} register={register} />
</Grid>
);
})}
</Grid>
</AppCard>
</form>
);
};
N.B. The images are the same because the forms will contain the same information, i.e. the same form fields by name.
Entry for first form:
Entry for the second form:
Each form is created with a config like this:
{
component: DynamicForm,
label: "Stepper Label",
config: {
headline: "Form 1",
actionButtonText: "Next",
formName: 'form-name',
defaultValues: defaultConfigObject,
formFields: [
{
component: "AppTextInput",
config: {
label: "Field 1",
name: "field_1",
},
},
{
component: "AppTextInput",
config: {
label: "Field2",
name: "field_2",
},
},
{
component: "AppTextInput",
config: {
label: "Field3",
name: "field_3",
},
},
{
component: "AppTextInput",
config: {
label: "Field4",
name: "field4",
},
},
],
handleSubmit: (formData) => console.log(formData),
},
},
And the active component in the steps is handled like:
import { Button, createStyles, makeStyles, Theme } from "#material-ui/core";
import React, { useContext, useEffect, useState } from "react";
import { StepperContext } from "./StepperProvider";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
buttonsContainer: {
margin: theme.spacing(2),
},
buttons: {
display: "flex",
justifyContent: "space-between",
},
})
);
export const StepPanel = (props) => {
const { activeStep, steps, handleBack, handleNext, isFinalStep } = useContext(
StepperContext
);
const [activeComponent, setActiveComponent] = useState(steps[activeStep]);
const classes = useStyles();
useEffect(() => {
setActiveComponent(steps[activeStep]);
}, [activeStep]);
return (
<div>
<activeComponent.component {...activeComponent.config} />
{
isFinalStep ? (
<div className={classes.buttonsContainer}>
<div className={classes.buttons}>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={props.finishFunction}
>
Finish And Submit
</Button>
</div>
</div>
)
:
null
}
</div>
);
};
From your image, every form looks the same. You should try providing a unique key value to your component so React know that each form is different. In this case it can be the step number for example:
<activeComponent.component {...props} key='form-step-1'>

e.preventDefault prevents me from unit testing with Enzyme

I have a reactJS application with a button.
When user presses the button, the user gets verificationCode on their cellphone.
At first, the form was refreshing randomly when user presses the button but I figured out that I have to use
event.preventDefault();
to stop the button refreshing.
So in my onClick handler, I have the following structure.
CODE -
const handleOnClick = async(event) => {
event.preventDefault();
logic.. (which includes async calls to backend API)
}
However, my problem is that when I create an unit test with Enzyme, the function returns in 'preventDefault()' and never executes the logic.
Is there anyway to test unit test in this case?
import React, {useState} from 'react';
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
import {
isInputNumeric,
processKoreaCellphone
} from '../../../../api/auth/authUtils';
import {requestMobileVerificationCode} from "../../../../api/auth/authApiConsumer";
import Select from "#material-ui/core/Select";
import OutlinedInput from "#material-ui/core/OutlinedInput";
import MenuItem from "#material-ui/core/MenuItem";
import {makeStyles} from "#material-ui/core";
import Button from '#material-ui/core/Button';
import Typography from "#material-ui/core/Typography";
import Box from "#material-ui/core/Box";
import Link from '#material-ui/core/Link';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
import Collapse from '#material-ui/core/Collapse';
const useStyles = makeStyles(theme => ({
cellphoneCountryCodeStyle: {
marginTop: '8px',
marginBottom: '4px'
},
requestVerificationMsgBtnStyle: {
marginTop: '8px',
marginBottom: '4px',
minHeight: '40px',
},
txtLabel: {
paddingTop: '0px',
fontSize: '0.75rem',
color: 'rgba(0, 0, 0, 0.54)'
},
txtLabelGrid: {
paddingTop: '0px',
},
}));
export const CellphoneTextField = props => {
const {onStateChange} = props;
const [state, setState] = useState({
errors: [],
onChange: false,
pristine: false,
touched: false,
inProgress: false,
value: {
cellphoneCountryCode: '82',
cellphone: '',
},
verificationCode: [],
isLocked: false
});
const [open, setOpen] = useState(false);
const [verificationCode, setVerificationCode] = useState('');
const [isVerificationCodeError, setIsVerificationCodeError] = useState(false);
const handleOnClick = async (event) => {
const eventCurrentTarget = event.currentTarget.name;
if (eventCurrentTarget === 'resendBtn' || eventCurrentTarget
=== 'resetBtn') {
event.preventDefault();
}
if ((eventCurrentTarget === 'requestVerificationMsgBtn' && state.isLocked
=== false) || eventCurrentTarget === 'resendBtn') {
const updateState = {
...state,
isLocked: true,
inProgress: true,
};
setState(updateState);
onStateChange(updateState);
const lang = navigator.language;
const cellphoneCountryCode = state.value.cellphoneCountryCode;
const cellphone = state.value.cellphone;
const response = await requestMobileVerificationCode(lang,
cellphoneCountryCode, cellphone).catch(e => {});
const updatedState = {
...state,
isLocked: true,
inProgress: false,
verificationCode: state.verificationCode.concat(response),
};
setState(updatedState);
onStateChange(updatedState);
}
};
const classes = useStyles();
return (
<Grid container spacing={1}>
<Grid item xs={12} p={0} className={classes.txtLabelGrid}>
<Typography className={classes.txtLabel} component="h5" id="infoMsg"
name="infoMsg"
variant="caption">
Did not receive the code? <Link
component="button"
variant="body2"
id="resendBtn"
name="resendBtn"
to="/"
className={classes.txtLabel}
onClick={handleOnClick}
>
[resend VericationCode]
</Link>
</Typography>
<Box m={1}/>
</Grid>
</Grid>
)
};
export default CellphoneTextField;
My unit test code
jest.mock("../../../../api/auth/authApiConsumer");
configure({adapter: new Adapter()});
describe('<CellphoneTextField />', () => {
const handleStateChange = jest.fn();
let shallow;
beforeAll(() => {
shallow = createShallow();
});
let wrapper;
beforeEach(() => {
wrapper = shallow(<CellphoneTextField onStateChange={handleStateChange}/>);
});
it('should allow user to resend verification code', (done) => {
act(() => {
wrapper.find('#resendBtn').simulate('click', {
currentTarget: {
name: 'resendBtn'
}
});
});
When I run the unit test, code beyond
event.preventDefault();
is not executed.
The second argument to .simulate('click', ...) is a mock event.
You need to pass in a no-op preventDefault function with that event when you simulate the click, because your code is trying to call e.preventDefault() but preventDefault doesn't exist on the (mock) event.
This should work:
wrapper.find('#resendBtn').simulate('click', {
preventDefault() {},
currentTarget: {
name: 'resendBtn'
}
});

Passing props to MUI styles

Given the Card code as in here. How can I update the card style or any material UI style as from:
const styles = theme => ({
card: {
minWidth: 275,
},
To such follows:
const styles = theme => ({
card: {
minWidth: 275, backgroundColor: props.color
},
when I tried the latest one, I got
Line 15: 'props' is not defined no-undef
when I updated code to be :
const styles = theme => (props) => ({
card: {
minWidth: 275, backgroundColor: props.color
},
also
const styles = (theme ,props) => ({
card: {
minWidth: 275, backgroundColor: props.color
},
Instead of
const styles = theme => ({
card: {
minWidth: 275, backgroundColor: props.color
},
I got the component card style at the web page messy.
By the way, I pass props as follows:
<SimpleCard backgroundColor="#f5f2ff" />
please help!
Deleted the old answer, because it's no reason for existence.
Here's what you want:
import React from 'react';
import { makeStyles } from '#material-ui/core';
const useStyles = makeStyles({
firstStyle: {
backgroundColor: props => props.backgroundColor,
},
secondStyle: {
color: props => props.color,
},
});
const MyComponent = ({children, ...props}) =>{
const { firstStyle, secondStyle } = useStyles(props);
return(
<div className={`${firstStyle} ${secondStyle}`}>
{children}
</div>
)
}
export default MyComponent;
Now you can use it like:
<MyComponent color="yellow" backgroundColor="purple">
Well done
</MyComponent>
Official Documentation
Solution for how to use both props and theme in material ui :
const useStyles = props => makeStyles( theme => ({
div: {
width: theme.spacing(props.units || 0)
}
}));
export default function ComponentExample({ children, ...props }){
const { div } = useStyles(props)();
return (
<div className={div}>{children}</div>
);
}
Here the Typescript solution:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import {Theme} from '#material-ui/core';
export interface StyleProps {
height: number;
}
const useStyles = makeStyles<Theme, StyleProps>(theme => ({
root: {
background: 'green',
height: ({height}) => height,
},
}));
export default function Hook() {
const props = {
height: 48
}
const classes = useStyles(props);
return <Button className={classes.root}>Styled with Hook API</Button>;
}
If you want to play with it, try it in this CodeSandbox
In MUI v5, this is how you access the props when creating the style object using styled():
import { styled } from "#mui/material";
const StyledBox = styled(Box)(({ theme, myColor }) => ({
backgroundColor: myColor,
width: 30,
height: 30
}));
For folks who use typescript, you also need to add the prop type to the CreateStyledComponent:
type DivProps = {
myColor: string;
};
const Div = styled(Box)<DivProps>(({ theme, myColor }) => ({
backgroundColor: myColor,
width: 30,
height: 30
}));
<StyledBox myColor="pink" />
If you want to use system props in your custom component like Box and Typography, you can use extendSxProp like the example below:
import { unstable_extendSxProp as extendSxProp } from "#mui/system";
const StyledDiv = styled("div")({});
function DivWithSystemProps(inProps) {
const { sx } = extendSxProp(inProps);
return <StyledDiv sx={sx} />;
}
<DivWithSystemProps
bgcolor="green"
width={30}
height={30}
border="solid 1px red"
/>
Explanation
styled("div")(): Add the sx props to your custom component
extendSxProp(props): Gather the top level system props and put it inside the sx property:
const props = { notSystemProps: true, color: 'green', bgcolor: 'red' };
const finalProps = extendSxProp(props);
// finalProps = {
// notSystemProps: true,
// sx: { color: 'green', bgcolor: 'red' }
// }
To use with typescript, you need to add the type for all system properties:
type DivSystemProps = SystemProps<Theme> & {
sx?: SxProps<Theme>;
};
function DivWithSystemProps(inProps: DivSystemProps) {
const { sx, ...other } = extendSxProp(inProps);
return <StyledDiv sx={sx} {...other} />;
}
Here's the official Material-UI demo.
And here's a very simple example. It uses syntax similar to Styled Components:
import React from "react";
import { makeStyles, Button } from "#material-ui/core";
const useStyles = makeStyles({
root: {
background: props => props.color,
"&:hover": {
background: props => props.hover
}
},
label: { fontFamily: props => props.font }
});
export function MyButton(props) {
const classes = useStyles(props);
return <Button className={classes.root} classes={{ label: classes.label }}>My Button</Button>;
}
// and the JSX...
<MyButton color="red" hover="blue" font="Comic Sans MS" />
This demo uses makeStyles, but this feature is also available in styled and withStyles.
This was first introduced in #material-ui/styles on Nov 3, 2018 and was included in #material-ui/core starting with version 4.
This answer was written prior to version 4.0 severely out of date!
Seriously, if you're styling a function component, use makeStyles.
The answer from James Tan is the best answer for version 4.x
Anything below here is ancient:
When you're using withStyles, you have access to the theme, but not props.
Please note that there is an open issue on Github requesting this feature and some of the comments may point you to an alternative solution that may interest you.
One way to change the background color of a card using props would be to set this property using inline styles. I've forked your original codesandbox with a few changes, you can view the modified version to see this in action.
Here's what I did:
Render the component with a backgroundColor prop:
// in index.js
if (rootElement) {
render(<Demo backgroundColor="#f00" />, rootElement);
}
Use this prop to apply an inline style to the card:
function SimpleCard(props) {
// in demo.js
const { classes, backgroundColor } = props;
const bull = <span className={classes.bullet}>•</span>;
return (
<div>
<Card className={classes.card} style={{ backgroundColor }}>
<CardContent>
// etc
Now the rendered Card component has a red (#F00) background
Take a look at the Overrides section of the documentation for other options.
#mui v5
You can use styled() utility (Make sure that you're importing the correct one) and shouldForwardProp option.
In the following example SomeProps passed to a div component
import { styled } from '#mui/material'
interface SomeProps {
backgroundColor: 'red'|'blue',
width: number
}
const CustomDiv = styled('div', { shouldForwardProp: (prop) => prop !== 'someProps' })<{
someProps: SomeProps;
}>(({ theme, someProps }) => {
return ({
backgroundColor: someProps.backgroundColor,
width: `${someProps.width}em`,
margin:theme.spacing(1)
})
})
import React from "react";
import { makeStyles } from "#material-ui/styles";
import Button from "#material-ui/core/Button";
const useStyles = makeStyles({
root: {
background: props => props.color,
"&:hover": {
background: props => props.hover
}
}
});
export function MyButton(props) {
const classes = useStyles({color: 'red', hover: 'green'});
return <Button className={classes.root}>My Button</Button>;
}
Was missing from this thread a props use within withStyles (and lead to think it wasn't supported)
But this worked for me (say for styling a MenuItem):
const StyledMenuItem = withStyles((theme) => ({
root: {
'&:focus': {
backgroundColor: props => props.focusBackground,
'& .MuiListItemIcon-root, & .MuiListItemText-primary': {
color: props => props.focusColor,
},
},
},
}))(MenuItem);
And then use it as so:
<StyledMenuItem focusColor={'red'} focusBackground={'green'}... >...</StyledMenuItem>
I spent a couple of hours trying to get withStyles to work with passing properties in Typescript. None of the solutions I found online worked with what I was trying to do, so I ended up knitting my own solution together, with snippets from here and there.
This should work if you have external components from, lets say Material UI, that you want to give a default style, but you also want to reuse it by passing different styling options to the component:
import * as React from 'react';
import { Theme, createStyles, makeStyles } from '#material-ui/core/styles';
import { TableCell, TableCellProps } from '#material-ui/core';
type Props = {
backgroundColor?: string
}
const useStyles = makeStyles<Theme, Props>(theme =>
createStyles({
head: {
backgroundColor: ({ backgroundColor }) => backgroundColor || theme.palette.common.black,
color: theme.palette.common.white,
fontSize: 13
},
body: {
fontSize: 12,
},
})
);
export function StyledTableCell(props: Props & Omit<TableCellProps, keyof Props>) {
const classes = useStyles(props);
return <TableCell classes={classes} {...props} />;
}
It may not be the perfect solution, but it seems to work. It's a real bugger that they haven't just amended withStyles to accept properties. It would make things a lot easier.
Solution for TypeScript with Class Component:
type PropsBeforeStyle = {
propA: string;
propB: number;
}
const styles = (theme: Theme) => createStyles({
root: {
color: (props: PropsBeforeStyle) => {}
}
});
type Props = PropsBeforeStyle & WithStyles<typeof styles>;
class MyClassComponent extends Component<Props> {...}
export default withStyles(styles)(MyClassComponent);
export const renderButton = (tag, method, color) => {
const OkButton = withStyles({
root: {
"color": `${color}`,
"filter": "opacity(0.5)",
"textShadow": "0 0 3px #24fda39a",
"backgroundColor": "none",
"borderRadius": "2px solid #24fda3c9",
"outline": "none",
"border": "2px solid #24fda3c9",
"&:hover": {
color: "#24fda3c9",
border: "2px solid #24fda3c9",
filter: "opacity(1)",
},
"&:active": {
outline: "none",
},
"&:focus": {
outline: "none",
},
},
})(Button);
return (
<OkButton tag={tag} color={color} fullWidth onClick={method}>
{tag}
</OkButton>
);
};
renderButton('Submit', toggleAlert, 'red')
Here's another way of dynamically passing props to hook's API in MUI v5
import React from "react";
import { makeStyles } from "#mui/styles";
import { Theme } from "#mui/material";
interface StyleProps {
height: number;
backgroundColor: string;
}
const useStyles = makeStyles<Theme>((theme) => ({
root: ({ height, backgroundColor }: StyleProps) => ({
background: backgroundColor,
height: height
})
}));
export default function Hook() {
const props = {
height: 58,
backgroundColor: "red"
};
const classes = useStyles(props);
return (
<button className={classes.root}>
another way of passing props to useStyle hooks
</button>
);
}
here's the codesandbox https://codesandbox.io/s/styles-with-props-forked-gx3bf?file=/demo.tsx:0-607
Here's 2 full working examples of how to pass props to MUI v5 styles. Either using css or javascript object syntax.
With css syntax:
import { styled } from '#mui/system'
interface Props {
myColor: string
}
const MyComponent = ({ myColor }: Props) => {
const MyStyledComponent = styled('div')`
background-color: ${myColor};
.my-paragraph {
color: white;
}
`
return (
<MyStyledComponent >
<p className="my-paragraph">Hello there</p>
</MyStyledComponent >
)
}
export default MyComponent
Note that we define MyStyledComponent within MyComponent, making the scoped props available to use in the template string of the styled() function.
Same thing with javascript object syntax:
import { styled } from '#mui/system'
const MyComponent = ({ className }: any) => {
return (
<div className={className}>
<p className="my-paragraph">Hello there</p>
</div>
)
}
interface Props {
myColor: string
}
const MyStyledComponent = styled(MyComponent)((props: Props) => ({
backgroundColor: props.myColor,
'.my-paragraph': { color: 'white' },
}))
export default MyStyledComponent
For this second example, please note how we pass the className to the component we want to apply the styles to. The styled() function will pass a className prop with the styles you define. You typically want to apply that to your root element. In this case the div.
Result:
I'm sure there's other variations of how to do this, but these two are easy to implement and understand.
You might need to memoize the calculated styles, and maybe not use this approach if your props changes a lot. I don't think it's very performant.
Using styled-components, sometimes you only want to apply styles if the prop is passed, in such cases you can do something like this (remove : {foo: boolean} if not using TypeScript):
const SAnchor = styled("a", {
shouldForwardProp: prop => prop !== "foo",
})(({foo = false}: {foo: boolean}) => ({
...(foo && {
color: "inherit",
textDecoration: "none",
}),
}));
Object spread source
shouldForwardProp docs
#mui v5
I use theme and prop from JSON object
const Answerdiv = styled((props) => {
const { item_style, ...other } = props;
return <div {...other}></div>;
})(({ theme, item_style }) => {
for(var i of Object.keys(item_style)){
item_style[i] =Object.byString(theme,item_style[i])|| item_style[i];
}
return (item_style)
});
component use
<Answerdiv item_style={(item_style ? JSON.parse(item_style) : {})}>
for Object.byString
Object.byString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}

Resources