React material-ui (MUI) 5 conditional CSS classes - reactjs

I'm moving from React material-ui 4 to MUI 5.
How do I achieve this type of pattern using the new styled API (or whatever makes sense)?
I'm using Typescript.
const useStyles = makeStyles(theme => ({
topBar: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: TRANSITION_DURATION,
}),
marginLeft: DRAWER_WIDTH,
},
topBarShift: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: TRANSITION_DURATION,
}),
marginLeft: 0,
},
}));
function Header({ drawer }: IHeader) {
const classes = useStyles();
...
return (
<div className={`${classes.topBar} ${!drawer && classes.topBarShift}`}>
...
</div>
);
}

If I understand your question clearly, you just want a conditional string.
This can be done by creating a util function for reusability:
type ConditionalClasses = {
[className: string]: bool
}
const conditionalClass = (classes: ConditionalClass) =>
Object.keys(classes).reduce((combinedClassName, className) => classes[className] ? `${combinedClassName} ${className}` : combinedClassName, "")
Usage goes as follows
// Output: "a"
const newClassName = conditionalClasses({a: true, b: false})
Alternatively, you could use clsx
-- Edit
Looks like I misread and hence misunderstood the question.
If you want to use the styled API styles conditionally, you can use the overridesResolver option provided by the styled API.
const CustomDivComponent = styled("div", {
overridesResolver: (props, styles) => {
// Do your conditional logic here.
// Return the new style.
return {};
}
})(({ theme }) => ({
// Your original styles here...
}));
More documentation can be found here

Related

How to implement react-dnd useDragLayer?

I have a component that currently uses the useDrag hook to connect to react-dnd. It works well, except for previews. I want to implement useDragLayer instead to see if it would help with my preview problems, as many online threads suggest.
This is my current (simplified) useDrag implementation:
const [{ isDragging }, connectDragSource, connectPreview] = useDrag({
item,
collect: monitor => ({
isDragging: monitor.getItem()?.index === item.index,
})
})
return (
<Wrapper ref={connectPreview} isDragging={isDragging}>
<DragHandle ref={connectDragSource} />
</Wrapper>
)
How do I use useDragLayer in this context, in a way that might help with my previews? The docs example makes little sense to me...
How do I connect my rendered components using useDragLayer api? useDragLayer doesn't return drag source and preview connector functions (like useDrag does on index 1 and 2 of the returned array), and its collect function doesn't provide a DragSourceConnector instance either. So what do I do with the hook/returned value after I call it?
I just resolved this and want to share it to help others :)
You will need to do couple of things for this to fully work.
Disable the default preview behavior by adding the following useEffect
import { getEmptyImage } from "react-dnd-html5-backend";
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: "BOX",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
Create the custom default layer
export const CustomDragLayer = (props: {}) => {
const {
itemType,
isDragging,
initialCursorOffset,
initialFileOffset,
currentFileOffset,
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialCursorOffset: monitor.getInitialClientOffset(),
initialFileOffset: monitor.getInitialSourceClientOffset(),
currentFileOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(
initialCursorOffset,
initialFileOffset,
currentFileOffset
)}
>
<div>Your custom drag preview component logic here</div>
</div>
</div>
);
};
const layerStyles: CSSProperties = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
border: "10px solid red",
};
function getItemStyles(
initialCursorOffset: XYCoord | null,
initialOffset: XYCoord | null,
currentOffset: XYCoord | null
) {
if (!initialOffset || !currentOffset || !initialCursorOffset) {
return {
display: "none",
};
}
const x = initialCursorOffset?.x + (currentOffset.x - initialOffset.x);
const y = initialCursorOffset?.y + (currentOffset.y - initialOffset.y);
const transform = `translate(${x}px, ${y}px)`;
return {
transform,
WebkitTransform: transform,
background: "red",
width: "200px",
};
}
Add the <CustomDragLayer /> to the top-level component
You will need to include the ref={drag} to the component you want to drag and remove the connectPreview ref completely.
Hopefully, this helps you.

JSS keyframes not working when passing props

I have a Spinner component that's basically a loading icon. I'm trying to pass props to the JSS styles so that it can be customized. But the animations don't seem to work if I pass props to the keyframes.
Below is the component. When I use the animation $spinnertest it works fine. If I use $spinners, it doesn't load the animation (when inspecting the elements, animation-name doesn't even show up in the class, leading me to believe it doesn't get generated. ).
**Example CodeSandBox of issue (just change animation to spinners): https://codesandbox.io/s/exciting-shirley-pqt1o?fontsize=14&hidenavigation=1&theme=dark
const useStyles = makeStyles(theme => ({
root: props => ({
width: props.size,
height: props.size,
position: 'relative',
contain: 'paint',
display: 'inline-block',
}),
spinner: props => ({
width: props.size*0.3125,
height: props.size*0.3125,
background: props.color,
position: 'absolute',
animationDuration: props.duration,
animationIterationCount: 'infinite',
animationTimingFunction: 'ease-in-out',
}),
spinnerAnimation: {
animationName: '$spinners',
},
square2: props => ({
animationDelay: -props.duration/2,
}),
'#keyframes spinnertest': {
'25%': {
transform: 'translateX(22px) rotate(-90deg) scale(.5)',
},
'50%': {
transform: 'translateX(22px) translateY(22px) rotate(-180deg)',
},
'75%': {
transform: 'translateX(0) translateY(22px) rotate(-270deg) scale(.5)',
},
'to': {
transform: 'rotate(-1turn)',
},
},
'#keyframes spinners': props => ({
'25%': {
transform: `translateX(${props.translate}px) rotate(-90deg) scale(.5)`,
},
'50%': {
transform: `translateX(${props.translate}px) translateY(${props.translate}px) rotate(-180deg)`,
},
'75%': {
transform: `translateX(0) translateY(${props.translate}px) rotate(-270deg) scale(.5)`,
},
'to': {
transform: `rotate(-1turn)`,
},
}),
}));
export default function Spinner(props) {
const {duration, size, color} = props;
const classes = useStyles({
duration: duration,
size: size,
color: color,
translate: size*(1-0.3125),
});
return (
<Box className={classes.root}>
<Box className={clsx(classes.spinner, classes.spinnerAnimation)} />
<Box className={clsx(classes.spinner, classes.square2, classes.spinnerAnimation)} />
</Box>
)
}
Spinner.defaultProps = {
duration: 1800,
size: 32,
color: #fff,
}
I have a turnaround solution, which works (not that pretty). You would turn your withStyles into a currying function, that takes keyframesProps, and at your key frame definition you would use an IIFE that returns the object with its properties:
const useStyles = keyframesProps => makeStyles((theme) => ({
... all other styles,
// you need to call an IIFE because keyframes doesn't receive a function
"#keyframes spinners": ((props) => ({
"25%": {
transform: `translateX(${props.translate}px) rotate(-90deg) scale(.5)`
},
"50%": {
transform: `translateX(${props.translate}px) translateY(${props.translate}px) rotate(-180deg)`
},
"75%": {
transform: `translateX(0) translateY(${props.translate}px) rotate(-270deg) scale(.5)`
},
to: {
transform: `rotate(-1turn)`
}
}))(keyframesProps)
}));
at your component you would define your classes like:
const styleProps = {
duration: duration,
size: size,
color: color
}
const framesProps = {
translate: size * (1 - 0.3125)
}
const classes = useStyles(framesProps)(styleProps);
It sounds that MUI has a bug around props in makeStyles #keyframes
#16673
as Olivier Tassinari stated, this bug will be fixed in v5 where MUI gonna use a new styling solution styled-components RCF #22342.
The problem is even more general:
The arrow functions (with or without props) do not work within makeStyles
#21011
Passing the props to rules in your defined keyframes will fix it (after v5 has been available, hopefully)
"#keyframes spinners": {
"25%": {
transform: (props) =>
// console.log(props) and template generation will be created correctly.
`translateX(${props.translate}px) rotate(-90deg) scale(.5)`
},
// ...
}
Until then you can use higher-order useStyle creator for embedding your keyframes, as #buzatto suggested.
Or define your animation presets in your theme object and uses them globally around your project.
const theme = createMuiTheme({
animation: {
presets: {
duration: 180,
// or even function
rotateDeg: (angle) => `{angle}deg`
//...
}
}
});
// usage
const useStyles = makeStyles(theme => ({
"#keyframes spinners": {
"25%": {
transform: `translateX(${
theme.animation.presets.duration * 10
}px) rotate(${theme.animation.presets.rotateDeg(-90)}) scale(.5)`,
},
},
}

ReactJS hooks - drag and drop with multiple useState hooks and styled-components

I am fairly new to hooks and I am trying to implement a drag and drop container component that handles onDragStart, onDrag and onDragEnd functions throughout the mouse movement. I have been trying to replicate the code found here using hooks : https://medium.com/#crazypixel/mastering-drag-drop-with-reactjs-part-01-39bed3d40a03
I have almost got it working using the code below. It is animated using styled components. The issue is it works only if you move the mouse slowly. If you move the mouse quickly the SVG or whatever is contained in this div is thrown of the screen.
I have a component.js file that looks like
import React, { useState, useEffect, useCallback } from 'react';
import { Container } from './style'
const Draggable = ({children, onDragStart, onDrag, onDragEnd, xPixels, yPixels, radius}) => {
const [isDragging, setIsDragging] = useState(false);
const [original, setOriginal] = useState({
x: 0,
y: 0
});
const [translate, setTranslate] = useState({
x: xPixels,
y: yPixels
});
const [lastTranslate, setLastTranslate] = useState({
x: xPixels,
y: yPixels
});
useEffect(() =>{
setTranslate({
x: xPixels,
y: yPixels
});
setLastTranslate({
x: xPixels,
y: yPixels
})
}, [xPixels, yPixels]);
const handleMouseMove = useCallback(({ clientX, clientY }) => {
if (!isDragging) {
return;
}
setTranslate({
x: clientX - original.x + lastTranslate.x,
y: clientY - original.y + lastTranslate.y
});
}, [isDragging, original, lastTranslate, translate]);
const handleMouseUp = useCallback(() => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
setOriginal({
x:0,
y:0
});
setLastTranslate({
x: translate.x,
y: translate.y
});
setIsDragging(false);
if (onDragEnd) {
onDragEnd();
}
}, [isDragging, translate, lastTranslate]);
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp)
};
}, [handleMouseMove, handleMouseUp]);
const handleMouseDown = ({ clientX, clientY }) =>{
if (onDragStart) {
onDragStart();
}
setOriginal({
x: clientX,
y: clientY
});
setIsDragging(true);
};
return(
<Container
onMouseDown={handleMouseDown}
x={translate.x}
y={translate.y}
{...{radius}}
isDragging={isDragging}
>
{children}
</Container>
)
};
export default Draggable
And the styled components file, styled.js looks like the following:
import styled from 'styled-components/macro';
const Container = styled.div.attrs({
style: ({x,y, radius}) => ({
transform: `translate(${x - radius}px, ${y - radius}px)`
})
})`
//cursor: grab;
position: absolute;
${({isDragging}) =>
isDragging && `
opacity: 0.8
cursor: grabbing
`}
`;
export {
Container
}
So I pass in the initial value from the parent initially. I think i am not dealing with the useEffect / useState correctly and it is not getting the information fast enough.
I would be extremely grateful if someone can help me figure out how to fix this issue. Apologies again, but I am very new to using hooks.
Thanks You :)
Ideally, since setState is asynchronous you'd move all your state into one object (as the medium example does). Then, you can leverage the setState callback to make sure the values that each event listener and event callback is using are up-to-date when setState is called.
I think the example in that medium article had the same jumping issue (which is probably why the example video moved the objects slowly), but without a working example, it's hard to say. That said, to resolve the issue, I removed the originalX, originalY, lastTranslateX, lastTranslateY values as they're not needed since we're leveraging the setState callback.
Furthermore, I simplified the event listeners/callbacks to:
mousedown => mouse left click hold sets isDragging true
mousemove => mouse movement updates translateX and translateY via clientX and clientY updates
mouseup => mouse left click release sets isDragging to false.
This ensures that only one event listener is actually transforming x and y values.
If you want to leverage this example to include multiple circles, then you'll need to either reuse the component below OR use useRef and utilize the refs to move the circle that is selected; however, that's beyond the scope of your original question.
Lastly, I also fixed a styled-components deprecation issue by restructuring the styled.div.data.attr to be a function that returns a style property with CSS, instead of an object with a style property that is a function that returns CSS.
Deprecated:
styled.div.attrs({
style: ({ x, y, radius }) => ({
transform: `translate(${x - radius}px, ${y - radius}px)`
})
})`
Updated:
styled.div.attrs(({ x, y, radius }) => ({
style: {
transform: `translate(${x - radius}px, ${y - radius}px)`
}
}))`
Working example:
components/Circle
import styled from "styled-components";
const Circle = styled.div.attrs(({ x, y, radius }) => ({
style: {
transform: `translate(${x - radius}px, ${y - radius}px)`
}
}))`
cursor: grab;
position: absolute;
width: 25px;
height: 25px;
background-color: red;
border-radius: 50%;
${({ isDragging }) =>
isDragging &&
`
opacity: 0.8;
cursor: grabbing;
`}
`;
export default Circle;
components/Draggable
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Circle from "../Circle";
const Draggable = ({ position, radius }) => {
const [state, setState] = useState({
isDragging: false,
translateX: position.x,
translateY: position.y
});
// mouse move
const handleMouseMove = useCallback(
({ clientX, clientY }) => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
translateX: clientX,
translateY: clientY
}));
}
},
[state.isDragging]
);
// mouse left click release
const handleMouseUp = useCallback(() => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
isDragging: false
}));
}
}, [state.isDragging]);
// mouse left click hold
const handleMouseDown = useCallback(() => {
setState(prevState => ({
...prevState,
isDragging: true
}));
}, []);
// adding/cleaning up mouse event listeners
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
return (
<Circle
isDragging={state.isDragging}
onMouseDown={handleMouseDown}
radius={radius}
x={state.translateX}
y={state.translateY}
/>
);
};
// prop type schema
Draggable.propTypes = {
position: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number
}),
radius: PropTypes.number
};
// default props if none are supplied
Draggable.defaultProps = {
position: {
x: 20,
y: 20
},
radius: 10,
};
export default Draggable;

React - prevstate issues, getting back old value

I have an SVG map with paths, and those paths change colors when I hover over them.
It changes state of specific section, for example my state looks like that:
POL3139: {
color: '#fbb9c5'
},
I am trying to switch back to the base color after I leave the path.
Here I am changing
onHover = (event) => {
event.stopPropagation();
const e = event.target.id
this.setState(prevState => ({
[e]: {
...prevState,
color: '#650df9'
},
}));
}
It totally works and changes my color to the picked one.
But then I am trying to revert back to the original one.
I tried that by making a base color in the state:
POL3139: {
color: '#fbb9c5',
base: '#fbb9c5'
},
and then onMouseLeave:
onLeave = (event) => {
event.stopPropagation();
const e = event.target.id;
this.setState(prevState => ({
[e]: {
...prevState,
// color: prevState.base - doesn't work
// color: prevState.[e].base - doesn't work
// color: [prevState.e.base] - doesn't work
color: 'pink'
}
}));
}
I was trying many possible solutions but I can't get it to work.
I am still learning react and it might be an easy one but I can't figure it out.
I don't think you're deconstructing your prevState properly.
Here's an example to illustrate how to deconstruct:
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
state = {
style: {
color: "black",
base: "black",
cursor: "pointer"
}
};
handleMouseEnter = e => {
const { style } = this.state;
this.setState({ style: { ...style, color: "red" } });
};
handleMouseLeave = e => {
const { style } = this.state;
this.setState({ style: { ...style, color: style.base } });
};
render() {
return (
<div
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={this.state.style}
>
<h1>hello</h1>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In this case style is equivalent to your [e].
In particular, look at the deconstructing here:
this.setState({ style: { ...style, color: style.base } });
There's a working example here.

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