Arbitrarily injecting styles with styled-components - reactjs

I'm populating a grid with various controls (in this example: up-down counter and a text box).
Currently, I'm injecting styles in the cls member (in this example can be e.g. wide-input and narrow-input):
render(): ReactNode {
const input: CellItem[] = [
{ isUpdown: false, cls: 'wide-input' },
{ isUpdown: true, cls: 'narrow-input' },
];
return (
<GridContainer>
input.map(content, index): ReactNode => {
return (
content.isUpdown ?
<StyledUpdownCell className={content.cls} /> :
<StyledTextBoxCell className={content.cls} /> :
)
}
</GridContainer>
);
}
My question is what is the proper way to do it using styled-components?
Is there a way to inject any arbitrary style (content.cls in this example, but tomorrow it could be also setting custom border color for instance)

Using styled components, you can have access to props passed to your custom styled component.
So, you could create different 'themes' props which you pass to your StyledUpdownCell and then access those inside the component styles. For example
const StyledUpdownCell = styled.div`
border-color: ${props => props.warningTheme ? 'red' : 'black'};
`
in use:
<StyledUpdownCell warningTheme />
You could also pass props directly but with a default e.g.
const StyledUpdownCell = styled.div`
border-color: ${props => props.borderColor || 'black'};
`
in use:
<StyledUpdownCell borderColor="violet" />
It's really up to you and how you want to design your component's API.
Side note: I've found this little library helpful for when creating components which have a lot of different props: https://github.com/yldio/styled-is

Related

Bordercolor override in Select component material ui on error

I need to override select component with border color to orange on error. Globally the outlined input bordercolor within my application is provided green. Only on error the border color has to overridden
import { FormControl, makeStyles, Select, withStyles } from '#material-ui/core';
const useStyles = makeStyles((theme) => ({
select: {
'&:before': {
borderColor: 'red',
},
'&:after': {
borderColor: 'red',
},
},
}));
const CustomSelect = ({
id,
value,
name,
variant,
onChange,
error,
priceType,
}) => {
const classes = useStyles();
return (
<FormControl size='small' error width='154px'>
<Select
id='price-type'
value={priceType}
native
name='priceType'
variant='outlined'
onChange={onChange}
error={true}
inputProps={{
style: {
width: '154px',
},
}}
className={classes.select}
>
<option value={''}></option>
<option value={'Retail'}>Retail</option>
<option value={'X-East'}>X-East</option>
</Select>
</FormControl>
);
};
export default CustomSelect;
General approach
Usually in MaterialUI when we want to override MaterialUI styles that are applied conditionally, we may use the classes prop:
classes={{ root: <class to apply always>, error: <class to apply in error state>}}
You can read about the CSS API and which classes are applicable for a given component at the bottom of its API page.
Alternatively, we can use a more elaborate approach and simply apply a rule whenever the component is in error state:
className={ error ? <class to apply in error state> : undefined }
The Select component
The Select component seems to be written at another time than most of the other MaterialUI input components. This is my intuitive impression based on API and code style but I might be wrong. Nevertheless, the Select component uses one of Input, FilledInput or OutlinedInput depending on the value of the variant property. Since you use outlined, I will continue this answer with OutlinedInput in mind. I will also use the color green for the border instead of orange because its distinction from red is more clear.
Naively, I would tackle the problem by looking at the CSS API of the Select component, and then also look closer at the OutlinedInput to which remaining properties of the Select component are spread. No applicable CSS API class exists on the Select component to reach your goal, but the OutlinedInput has a class error with the following description:
Pseudo-class applied to the root element if error={true}.
If we inspect the OutlinedInput in the development tools, we can see that the element that has the border is a fieldset residing inside the root element of the OutlinedInput (note: not the root element of the Select). If we use the error class, we will end up with an additional class applied to the root element of the OutlinedInput which is not what we want. Nonetheless, we could apply a class that refers to the aforementioned child element... this would work.
There is also a css class notchedOutline which we can see is applied to this fieldset resulting in the border. The dilemma here is that if we would be able to add some css to this class, then it would be applied at all times on the fieldset, but we want to apply it ONLY in error state.
This gives us four possible strategies to affect the css via the CSS API:
Add css conditionally (when in error) to the root class of the underlying OutlinedInput that targets child notchedOutline.
Add css to the root class of the underlying OutlinedInput that targets error state and child notchedOutline.
Add css to the error class of the underlying OutlinedInput and target child notchedOutline.
Add css conditionally (when in error) to the notchedOutline class of the underlying OutlinedInput.
Note that the second one and third are preferable in that they do not require us to add a class conditionally; this is handled by MaterialUI.
In the best of worlds, I would want the Select component to forward all of its non-existing props to the element it uses internally, in this case an OutlinedInput, but apparently, it does not forward portions of elements that it partially supports (the classes prop). This means that if we supply classes.error or classes.notchedOutline, MaterialUI will complain with
Material-UI: The key `XXXX` provided to the classes prop is not implemented in ForwardRef(Select).
instead of forwarding the prop to the underlying OutlinedInput. I do not know if this is a bug or not, but this is how it works currently.
This implies that we can not use any of the above four strategies via the classes prop of the Select component. A workaround is to use the Select className prop which is forwarded to the OutlinedInput which in turn forwards it to InputBase which forwards it to the native element, which is the same as the root element of the OutlinedInput. So, if we could access the underlying OutlinedInput itself, we could use all of the above strategies, otherwise only the first two (and then via className prop of the Select).
The solutions
1. Set the global error color of the entire app
If you want the error color to be something else than red for the entire app, then provide a theme which changes the color used for errors. This is the best approach since it also changes the color of helper texts and labels which would otherwise have a different color than the border of your Select input.
import green from '#material-ui/core/colors/green';
const theme = createTheme({
palette: {
error: {
main: green[500]
},
},
});
This is a clean and non-hacky way to do it and the one I recommend. However, if you DON'T want the general color for errors to be changed, this might not be a good solution, even though you could wrap just a local part of the app in a theme of its own.
2. Override the class used for notchedOutline in error state
This is a broad way to solve it since you can do this in several different ways using the four strategies described above. The downsides with all the solutions are that they are more or less hacky and that they only affect the border of the Select, implying that helper texts and labels will be differently colored than the border when the component is in error state (you could add styles to them too, making all of this very elaborate and hard to achieve).
2.a Global override / default for OutlinedInput
We can create a theme with a global override for the OutlinedInput. Since we then manipulate the OutlinedInput itself, we may use all four strategies from above.
Global override / default for OutlinedInput for root class targeting notchedOutline child
This theme would have to be applied conditionally only when the component is in error state. This is elaborate and not a good alternative.
Global override / default for OutlinedInput for root class targeting error state and notchedOutline child
const themeWithOverrides = createTheme({
overrides: {
MuiOutlinedInput: {
root: {
"&$error": {
"& $notchedOutline": {
border: "1px solid green"
}
}
}
}
}
})
This one works well and targets the OutlinedInput root class with css that applies only when it is in error state and applies to its notchedOutline child. The selectors refer to the css classes already defined by default in the theme for this component.
Global override / default for OutlinedInput for error class and notchedOutline child
const themeWithOverrides = createTheme({
overrides: {
MuiOutlinedInput: {
error: {
"& $notchedOutline": {
border: "1px solid green"
}
}
}
}
})
This solution results in an error message from MaterialUI
Material-UI: The `MuiOutlinedInput` component increases the CSS specificity of the `error` internal state.
which in turn recommends us to use the previous solution targeting the root class instead.
Global override / default for OutlinedInput for notchedOutline class
const themeWithOverrides = createTheme({
overrides: {
MuiOutlinedInput: {
notchedOutline: {
border: "1px solid green"
}
}
}
})
This will apply the style at all times which we don't want, so the theme has to be applied conditionally which is a hassle so this solution is not recommended at all.
2.b Css classes for Select component
Here we could also, in theory, use all of the four strategies described above. However, since we are not manipulating the OutlinedInput itself, and since MaterialUI doesn't forward the classes props that are not defined for the Select to the underlying component, only two remains.
Adding css class conditionally to className prop of the Select which is applied to the OutlinedInput root component targeting child notchedOutline
const useStyles = makeStyles((theme) => ({
errorState: {
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid green !important"
}
}
}))
...
<Select
...
className={ errorFlag ? classes.errorState : undefined }
...
</Select>
This solution both uses literal MaterialUI css classes and requires us to conditionally add the class. Finally, we have to use the !important modifier to override the specificity of the MaterialUI default behaviour. All in all, this has a slightly hacky flavour to it.
Adding css class to className prop of the Select which is applied to the OutlinedInput root component targeting child notchedOutline only in error state
const useStyles = makeStyles((theme) => ({
errorState: {
"&.Mui-error": {
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid green"
}
}
}
}))
...
<Select
...
className={ classes.errorState }
...
</Select>
This is slightly better because we don't need to conditionally apply the style. Also, as long as this style is injected after the MaterialUI default style, we can omit the !important modifier since this rule achieves the same specificity as the MaterialUI default behaviour. This method is absolutely preferable over the previous.
2.c Css classes for underlying OutlinedInput component via input prop on Select
The Select component takes a prop input which lets you define your own input components to be used. Here we can use a custom component that returns an OutlinedInput with the modifications you want.
export default function CustomInput(props) {
return (
<OutlinedInput
{...props}
<CHANGES HERE>
/>
)
}
Since we here can work directly towards the OutlinedInput, we can use its CSS API and thus use all the four strategies described earlier.
Note that to do it by the book, you should merge incoming classes.error and classes.notchedOutline with the classes you assign by default as well.
Adding css class conditionally to classes.root prop targeting child notchedOutline
Since we now have access to the classes.error prop, providing this class conditionally would not be a smart option.
Adding css class to classes.root prop targeting state error and child notchedOutline
Similarly, since we now have access to the classes.error prop, providing a class here targeting error state instead of targeting it directly would be an extra workaround and is not recommended.
Adding css class to classes.error prop targeting child notchedOutline
const useStyles = makeStyles((theme) => ({
error: {
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid green !important"
}
}
}))
export default function CustomInput(props) {
const classes = useStyles()
return (
<OutlinedInput
{...props}
classes={{ ...props.classes, error: classes.error }}
/>
)
}
This is a good alternative that allows us to leave the decision of whether the component is in error state or not to MaterialUI. Slightly hacky to use the literal MaterialUI css classes and that we have to provide the !important flag to override the higher specificity of the MaterialUI default behaviour.
Adding css class conditionally to classes.notchedOutline prop when in error state
const useStyles = makeStyles((theme) => ({
errorOutline: {
border: "1px solid green !important"
}
}))
export default function CustomInput(props) {
const classes = useStyles()
return (
<OutlinedInput
{...props}
classes={{ ...props.classes, notchedOutline: errorFlag ? classes.errorOutline : undefined }}
/>
)
}
This is equally good as the previous. It saves us from using literal
MaterialUI css classes but it forces us to add the class conditionally. Also here we have to provide the !important flag to override the higher specificity of the MaterialUI default behaviour.
Conclusion
It's a shame that it seems to be quite complicated to change such a simple thing. Nonetheless, the best solutions seem to be:
Use global error color override if you want to have a robust solution with uniform appearence for all inputs in your app that harmonize with labels and helper texts.
import green from '#material-ui/core/colors/green';
const theme = createTheme({
palette: {
error: {
main: green[500]
},
},
})
Use global override for OutlinedInput if you want a uniform error color across all OutlinedInputs and if you're ok with labels and helper texts that don't harmonize.
const themeWithOverrides = createTheme({
overrides: {
MuiOutlinedInput: {
root: {
"&$error": {
"& $notchedOutline": {
border: "1px solid green"
}
}
}
}
}
})
Use a custom input which conditionally overrides the classes.notchedOutline when the component is in error state if you need a one-time change on a single or couple of Selects / OutlinedInputs and if you can accept labels and helper texts that don't harmonize in color. I prefer this solution because I don't like to use literal MaterialUI css classes in my code.
const useStyles = makeStyles((theme) => ({
errorOutline: {
border: "1px solid green !important"
}
}))
export default function CustomInput(props) {
const classes = useStyles()
return (
<OutlinedInput
{...props}
classes={{ ...props.classes, notchedOutline: errorFlag ? classes.errorOutline : undefined }}
/>
)
}
...
<Select input={<CustomInput error={errorFlag} />} />
...
Here is a code sandbox with some of these recommended examples
You can use this styles
const useStyles = makeStyles((theme) => ({
select: {
borderColor:'#f00',
'&:focus': {
borderColor: '#f00',
},
},
}));

pass material ui styles to component - react

I am using a functional component with React, I need to show SVG Icon based on state and I want to load the relevant icon
so the parent will show only call :
<icon classes:... , state..></icon>
1- how can I pass style and if it does not exist and use a default style in the child?
now I have smth like in the parent :
... createStyle
IconSuccess: {
fontSize: 20,
width: 20,
},
IconWarning: {
fontSize: 20,
width: 20,
},
but i want smth like :
icon:{
width:..
font ..
warning: { color}
success: { color}
}
then
<IconChild state={state} classes={{ icon: itemStyle.icon}} />
this is work only if I pass specific style like:
<IconChild state={state} classes={{ iconWarning: itemStyle.iconWarning}} />
then in the childCOmponent I am doing smth like:
const classes = useStyles(props);
if( props.state == 1){
return <className={`${classes.iconWarning}`} />
}
else{
return <className={`${classes.iconSuccess}`} />
}
so basically I am trying to understand how to create a really generic component that I can use and pass and that need a state to choose the specific icon and also from specific class
do I need HOC ? or different approach
As I understand, you want to:
Reuse some common properties like width and fontSize.
Custom render other properties like color.
Then this is my approach:
First, make new style for commonly used properties.
Secondly, create new styles for conditional use of each state.
Last, use something like classnames to combine all classes.
So the main idea here is: instead of using one class for each item, now using two classes for each one. That's it!
const useStyles = withStyles({
commonProperty: {
fontSize: '20px',
width: '20px',
},
successOnlyProperty: {
color: 'green'
},
warningOnlyProperty: {
color: 'orange'
},
});

How to properly use TypeScript types with a Styled Component canvas on React

I am creating a React component on Gatsby using TypeScript and have defined a canvas Styled Component this way:
const Background = styled.canvas`
position: fixed;
width: 100%;
height: 100%;
`;
And to use it I am assigning types to it this way:
const canvas: HTMLCanvasElement = Background,
context = Background.getContext('2d');
But I am getting this error with the canvas type:
Type 'String & StyledComponentBase<"canvas", any, {}, never> & NonReactStatics<never, {}>' is missing the following properties from type 'HTMLCanvasElement': height, width, getContext, toBlob, and 238 more.
I am also getting an error on the .getContext() method:
This expression is not callable.
Type 'never' has no call signatures.
I have been searching for a solution but can not find a proper one for this specific problem.
Could someone please explain to me the best way to use Styled Component canvas with TypeScript?
The answer from #101arrowz was almost right but had a problem with the context that gave me this error.
Object is possibly 'null'.
But it helped me to find a different approach with the styles being inline and solve the problem this way:
const Component: React.FC = () => {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const [context, setContext] = React.useState<CanvasRenderingContext2D | null>(
null
);
React.useEffect(() => {
if (canvasRef.current) {
const renderCtx = canvasRef.current.getContext('2d');
if (renderCtx) {
setContext(renderCtx);
}
}
}, [context]);
return (
<div>
<canvas
id="canvas"
ref={canvasRef}
style={{
position: 'fixed',
width: '100%',
height: '100%'
}}
></canvas>
</div>
);
The Background is a React.Component (i.e. a function that creates a virtual element) rather than an HTMLCanvasElement. Not only does the function need to be called for it to even return anything remotely like an HTMLCanvasElement, but you also need access to the underlying DOM element to make it work. I do have a suggestion that you might be able to try, though.
import { useRef, useEffect } from 'react';
const Background = styled.canvas`
position: fixed;
width: 100%;
height: 100%;
`;
const ComponentUsingTheCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const context = canvasRef.current.getContext('2d');
// Do stuff with your canvas context here
});
return (
<Background ref={canvasRef} />
);
}
One note, I didn't type anything because TypeScript can do that automatically most of the time.
By the way, why use a styled component when you can just style inline? styled-components is only really useful if you do something with the props.

Apply radiobutton color using styled-components in Material UI?

In the Material UI documents, they provided sample code to show how one can change the color of a Radiobutton.
const GreenRadio = withStyles({
root: {
color: green[400],
'&$checked': {
color: green[600],
},
},
checked: {},
})(props => <Radio color="default" {...props} />);
I would like to replicate this with styled-component instead i.e. const StyledRadio = styled(Radio) but I am not too familiar with the syntax such as the ampersand and the dollar sign - how can I do this?
When using styled components with MUI, the CSS is applied to the root class of the component. If you need to apply a more specific style, then you'll need to target the relevant class. In this case, you'll need to target the .Mui-checked class:
const StyledRadio = styled(Radio)`
color: ${green[400]};
&.Mui-checked {
color: ${green[600]};
}
`;
The MUI docs are really good in that they list the CSS classnames for each component. If you visit the API docs for the Radio component, you'll see the .Mui-checked class listed there (under the 'Global Styles' column).
Here's a working example in Code Sandbox: https://codesandbox.io/embed/styled-components-9pewl
Here's the appropriate styled-components syntax:
const GreenRadio = styled(Radio)`
color: ${green[400]};
&.Mui-checked {
color: ${green[600]};
}
`;
Related documentation: https://material-ui.com/customization/components/#pseudo-classes

Warning: Received `false` for a non-boolean attribute. How do I pass a boolean for a custom boolean attribute?

Warning: Received `false` for a non-boolean attribute `comingsoon`.
If you want to write it to the DOM, pass a string instead:
comingsoon="false" or comingsoon={value.toString()}.
How do I pass a boolean in a custom attribute for React?
I'm using styled-components and passing the attribute through the component. Here is a picture of how I'm passing the attr.
passing boolean custom attr as "comingsoon"
styled-components css props
Try this instead:
comingsoon={value ? 1 : 0}
As of 5.1 you can now use transient props ($) which prevents the props being passed to the DOM element.
const Comp = styled.div`
color: ${props =>
props.$draggable || 'black'};
`;
render(
<Comp $draggable="red" draggable="true">
Drag me!
</Comp>
);
You have to add $ prefix to attribute:
$comingsoon={value}
Styled Components had added transient props in 5.1 version:
https://styled-components.com/docs/api#transient-props
In my case, it was because I was accidentally passing {...#props} down into a div.
Usually passing attribute={false} is fine, but not to native elements.
Similar to Frank Lins answer above but I had to use undefined instead of 0 to get rid of the warning:
comingsoon={value ? 1 : undefined}
Just make it a number instead, this is the workaround from https://github.com/styled-components/styled-components/issues/1198:
This error with styled-components appears to be due to styled() attempting to apply a boolean to an element in the DOM, but DOM elements only accept strings as attributes.
This is well documented in the styled-components repository here: https://github.com/styled-components/styled-components/issues/1198
There are two solutions:
Lift the styled component w/ the passed attribute up, so that the attribute is not applied to the element directly. Or,
Filter the passed attribute out of the props when calling styled components.
Both of these options are demonstrated in the code below.
CodeSandbox: https://codesandbox.io/s/cool-thunder-9w132?file=/src/App.tsx
import React, { useState } from "react";
import styled from 'styled-components';
// demonstration of two different solutions for solving the styled-components error:
// `Warning: Received `false` for a non-boolean attribute`
// Solution 1: Lift the attribute up into a wrapper.
// Solution 2: Filter-out the `toggle` attribute passed to styled-component.
interface BtnProps {
toggle: boolean;
}
const Container = styled.div`
width: 100%;
height: 500px;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
`;
const StyledBtnOne = styled.div<BtnProps>`
& button {
background-color: ${({toggle}) => toggle ? ' #2ecc71' : '' };
};
`;
const StyledBtnTwo = styled(({primary, ...props}) =>
<button {...(({toggle, ...propz}) => propz)(props)} />)<BtnProps>`
background-color: ${({toggle}) => toggle ? ' #3498db' : '' };
`;
const App = () => {
const [ btnOne, setBtnOne ] = useState(false);
const [ btnTwo, setBtnTwo ] = useState(false);
const triggerOne = () => setBtnOne(!btnOne);
const triggerTwo = () => setBtnTwo(!btnTwo);
return (
<Container>
<StyledBtnOne toggle={btnOne}>
<button
onClick={triggerOne}>
Solution 1
</button>
</StyledBtnOne>
<StyledBtnTwo
toggle={btnTwo}
onClick={triggerTwo}>
Solution 2
</StyledBtnTwo>
</Container>
);
}
export default App;
This warning can be caused also if the property of styled component has the name existing in HTML. For example I had such issue when named property wrap. After renaming warning disappeared.
Add "+" before your booleans values.
comingsoon = {+creator.comingSoon}
example below from the Link to answer
import styled from "styled-components";
import { Link } from "react-router";
const StyledLink = styled(Link)`
color: ${({ inverted }) => (inverted ? "white" : "chartreuse")};
`;
function Navbar() {
return (
<nav>
{# Bad #}
<StyledLink inverted={true}>Home</StyledLink>
{# Good #}
<StyledLink inverted={+true}>About</StyledLink>
</nav>
);
}
Solved this by enclosing with brackets {} the boolean variable I was passing through props.
const childrenWithProps = React.Children.map(props.children, child => {
return React.cloneElement(child, { showcard: { showCard } }
)});
I got this issue and also shows React Hydration Error in my Next.js application. In my case it seems Styled component got a custom component and it can't process 'boolean'.
Here is my workaround:
before:
styled(Text)<{ center?: boolean}>
// Text is my custom component
after:
styled.div<{ center?: boolean}>

Resources