Styled-components - dynamic CSS? - reactjs

I have a simple React app that gets CSS from an external source in JSON form, the styles look like this:
{
"DOMElements": [
{
"padding": "1rem",
"margin": "5rem",
},
{
"boxSizing": "border-box",
"height": "10px",
}
]
}
So when I get a response like above I want end up with something like this:
import styled from 'styled-components';
const DOMElement1 = styled.div`
padding: 1rem,
margin: 5rem,
`;
const DOMElement2 = styled.div`
boxSizing: border-box,
height: 10px,
`;
const Page = ({ children }) => (
<>
<DOMElement1>{children[1]}</DOMElement1>
<DOMElement2>{childrem[2]}</DOMElement2>
</>
);
It's worth noting that number of DOMElements in unknown, it might be 1, it might be 50.
The component part is easy, I can do just do indexed map and increase index every loop.
The problem I face is - how do I create dynamic styled-components props based on JSON response? I need to do it within the component itself as that's where I know how DOMElements look like, but styled-components are supposed to be outside of the component function... What am I missing? Is it even doable?

You can give props for styled components.
const DOMElement1 = styled.div`
padding: ${({padding}) => padding}rem,
margin: ${({margin}) => margin}rem,
`;
const Page = ({ children }) => (
<>
<DOMElement1 padding={valueFromJson} margin={valueFromJson}>{children[1]}</DOMElement1>
<DOMElement2 padding={valueFromJson} margin={valueFromJson}>{childrem[2]}</DOMElement2>
</>
);

Something like this:
const items = [
{
background: "blue",
width: "30px",
height: "30px",
padding:'10px'
},
{
height: "40px",
background: "red",
width: "30px",
padding:'10px'
}
];
const components = items.map((item) =>
styled("div")({
...item
})
);
export default function App() {
return (
<div style={{ background: "lightblue", width: "100vw", height: "100vh" }}>
{components.map(Comp => <Comp>hi</Comp>)}
</div>
);
}

A simple & straightforward solution would be taking props dynamically in your styled component and this way you don't need to worry about any incoming css property.
Refer the following example/ this snippet -
const DOMElement = styled.div`
${props => props}
`;
const Page = () => {
const data = {
"DOMElements": [
{
"padding": "1rem",
"margin": "5rem",
},
{
"boxSizing": "border-box",
"height": "10px",
}
]
};
return (
<>
{data?.DOMElements?.map((obj, index) =>
<DOMElement {...obj} key={index}>abcd</DOMElement>
)}
</>
);
}
Output snap -

Related

How to apply multiple classes to components on React 5?

Before v5, I was able to apply multiple class to the component like the following:
import React from "react";
import { makeStyles } from '#mui/styles';
const useStyles = makeStyles({
image: {
borderRadius: "50%",
width: 256,
height: 256,
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)",
},
}));
const MyComponent = (props) => {
const { data } = props;
const classes = useStyles();
return (
<img src={data.image} className={`${classes.image} ${classes.shadow}`} alt={data.title} />
);
};
export default MyComponent;
However, since the change of styling engine(?), it seems like I can only apply a single styling like:
import React from "react";
import { styled } from "#mui/system";
const ProfileImage = styled("img")({
borderRadius: "50%",
width: 256,
height: 256,
});
const MyComponent= (props) => {
const { data } = props;
return (
<ProfileImage src={data.image} alt={data.title} />
);
};
export default MyComponent;
Would there be any possible solution to achieve the same behavior as the one above?
Thank you!
You could use an object and just spread the properties you want to use
const classes = {
image: {
borderRadius: "50%",
width: 256,
height: 256
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)"
}
};
const ProfileImage1 = styled("img")({
...classes.image,
...classes.shadow
});
If you are using JS and want to benefit from type intellisense, you could use type annotation to get the hint of CSS properties
import { styled, SxProps, Theme } from "#mui/system";
/** #type{Record<string, SxProps<Theme>>} */
const classes = {
image: {
borderRadius: "50%",
width: 256,
height: 256
},
shadow: {
boxShadow: "-4px -3px 45px 21px rgba(0,0,0,0.35)"
}
};
Also, if you want to make the class conditional, styled component allows you to use prop from the component
const ProfileImage1 = styled("img")(({ shadow }) => ({
...classes.image,
...(shadow ? classes.shadow : {})
}));
export default function App() {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 5
}}
>
<ProfileImage1 src={"https://via.placeholder.com/256"} shadow />
<ProfileImage1 src={"https://via.placeholder.com/256"} />
</Box>
);
}

Chakra UI __css on sub-component isn't being applied when styling multipart components

I am using Typescript, React(create-react-app) and Chakra.
As in the title, css on <DropdownItem> component isn't being applied.
Css on <Dropdown> works normally.
I have this code basically copied from the styling multipart components and when comparing it to the List component from the Chakra source code, it looks okay to me. But it doesn't work for some reason.
There is no errors and when console logging the CSSDict from useStyles() and useMultiStyleConfig() it logs the styles normally. I think it must be breaking at __css={styles.item}.
Thank you in advance.
Select.tsx
<Dropdown onClick={() => setOpen(!open)}>
<DropdownItem>{currItem}</DropdownItem>
</Dropdown>
Dropdown.tsx
const [StylesProvider, useStyles] = createStylesContext("Dropdown");
export function Dropdown(props: any): any {
const { size, variant, children, ...rest } = props;
const styles = useMultiStyleConfig("Dropdown", { size, variant });
return (
<List __css={styles.dropdown} {...rest}>
<StylesProvider value={styles}>{children}</StylesProvider>
</List>
);
}
export function DropdownItem(props: any): any {
const styles = useStyles();
return <ListItem __css={styles.item} {...props} />;
}
theme.ts
export const theme = extendTheme({
components: {
Button,
Dropdown,
},
colors: {
main: {
independence: "#3D405B",
purplenavy: "#51557A",
darkbluegray: "#656A99",
terracotta: "#E07A5F",
eggshell: "#F4F1DE",
},
},
styles: {
global: () => ({
body: {
bg: "main.independence",
height: "100%",
},
}),
},
shadows: {
terra: "2px -2px #E07A5F",
eggshell: "2px -2px #F4F1DE",
},
});
database.style.ts
export const Dropdown: ComponentStyleConfig = {
parts: ["dropdown", "item"],
// The base styles for each part
baseStyle: {
dropdown: {
position: "relative",
display: "inline-block",
width: "100px",
cursor: "pointer",
fontSize: "calc(35px, 0.87vw)",
h: "100%",
zIndex: "10",
},
item: {
h: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
_hover: { bg: "main.violetLt", borderRadius: "15px 0 0 15px" },
fontWeight: "600",
bg: "orange",
},
},
// The size styles for each part
sizes: {},
// The variant styles for each part
variants: {},
// The default `size` or `variant` values
defaultProps: {},
};
Edit 1
Looking at it from React Devtools, <ListElement> is the last component that gets the correct __css prop, which then disappears in <li> child.
Edit 2
export function DropdownItem(props: any): any {
const styles = useStyles();
return <chakra.li __css={styles.item} {...props} />;
}
Using <chakra.li> instead of <ListElement> somehow solves the problem and the css is being passed and applied but I still don't understand why.

Material UI pagination - How Can I use custom style for number of pages?

I'm quite new to material-ui. I'm trying to build this component.
I was able to do the style for the next and previous buttons the same as in the picture.
The normal style shows the number of pages as a numbered group besides each other like this:
Are there any properties that I can pass for the pagination component, in which I can change the style?
Here is the code:
import Pagination from "#material-ui/lab/Pagination";
import useStyles from "./styles";
const ReviewsPagination = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Pagination count={8} />
</div>
);
};
export default ReviewsPagination;
and the style file:
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({
root: {
"& .MuiPagination-ul": {
"& > li:first-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
"& > li:last-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
},
},
});
export default useStyles;
Thank you!
you can use the usePagination hook to customize the pagination component. Like below:
export default function UsePagination() {
const classes = useStyles();
const { items } = usePagination({
count: 10,
});
return (
<nav>
<ul className={classes.ul}>
{items.map(({ page, type, selected, ...item }, index) => {
let children = null;
if (type === 'next' || type === 'previous') {
children = (
<button type="button" {...item}>
{type}
</button>
);
}else if(selected){
children = <div>{`${page}/10`}</div>
}
return <li key={index}>{children}</li>;
})}
</ul>
</nav>
);
}

Input file with label not updating state

I want to make input type file with custom style, I hide input and styling the label using css. There are 2 problems:
I chose file, but coverFileState is undefinded (if I remove css style and choose file then everything is okay).
I want to set file name inside label, but it isn't working. I set text using setCoverUploadText inside handleCoverChange, but no effect.
If you help me with that problem I will really appreciate this, thanks!
const useStyles = makeStyles((theme) => ({
fileInput: {
marginBottom: '1em',
width: '0.1px',
height: '0.1px',
opacity: '0',
overflow: "hidden",
position: "absolute"
},
uploadLabel: {
fontSize: '1rem',
cursor: 'pointer',
color: "gray",
border: "1px solid gray"
}
}));
export default function AddingBook(props: ParamsProps) {
const classes = useStyles();
const coverFile = useRef(null);
const [coverFileState, setCoverFile] = useState<string | Blob>();
const [coverUploadText, setCoverUploadText] = useState("Upload cover photo *");
const handleSubmit = () => {
if (coverFileState === undefined) {
setImageErrorMsg("*Please add cover photo.");
return;
}
api.post('/image/add', JSON.stringify(REST))
.then(res => {
uploadCoverPhoto();
props.close();
}).catch(err => {
const errorMsg = APIServices.onError(err);
showErrorPopup(errorMsg);
})
};
const handleCoverChange = () => {
// #ts-ignore
setCoverFile(coverFile.current.files[0]);
// #ts-ignore
setCoverUploadText(coverFile.current.files[0].name)
};
return (
<>
<div>
<input
id="copy-file-upload"
type="file"
accept="image/*"
ref={coverFile}
onChange={handleCoverChange}
data-testid="inputFile"
className={classes.fileInput}
/>
<label className={classes.uploadLabel} htmlFor="copy-file-upload">{coverUploadText}</label>
</div>
</>
);
Try to replace "className" by "style" for the input file and for the label.
Hope it will helps you
EDIT:
it worked for me when i set the style as :
const useStyles = {
fileInput: {
marginBottom: '1em',
width: '0.1px',
height: '0.1px',
opacity: '0',
overflow: "hidden",
position: "absolute"
},
uploadLabel: {
fontSize: '1rem',
cursor: 'pointer',
color: "gray",
border: "1px solid gray"
}
};
and use it in the return as :
....
<label style={useStyles.uploadLabel} htmlFor="copy-file-upload">{coverUploadText}</label>
....

React Create a Horizontal Divider with Text In between

I need to create a React component that is a Horizontal Divider with a content like text In between. All the resources I have online is unable to help me get this done. I tried a material-ui divider by creating a Divider component and placing my text in-between like the example below:
<Divider>Or</Divider>
But I get the error:
hr is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.
I need to achieve this in the image below:
Any help will be appreciated.
These are my codes below:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import List from '#material-ui/core/List';
import Divider from '#material-ui/core/Divider';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
}));
export default function ListDividers() {
const classes = useStyles();
return (
<List component="nav" className={classes.root} aria-label="mailbox
folders">
<Divider>Or</Divider>
</List>
);
}
Using Material UI.
import React from "react";
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles(theme => ({
container: {
display: "flex",
alignItems: "center"
},
border: {
borderBottom: "2px solid lightgray",
width: "100%"
},
content: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingRight: theme.spacing(2),
paddingLeft: theme.spacing(2),
fontWeight: 500,
fontSize: 22,
color: "lightgray"
}
}));
const DividerWithText = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.container}>
<div className={classes.border} />
<span className={classes.content}>{children}</span>
<div className={classes.border} />
</div>
);
};
export default DividerWithText;
You can import and use it like the below
<DividerWithText>Or</DividerWithText>
Result
Here a custom example of what you could do : repro on stackblitz
import React, { Component } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const App = () => {
return <Divider>Or</Divider>;
};
const Divider = ({ children }) => {
return (
<div className="container">
<div className="border" />
<span className="content">
{children}
</span>
<div className="border" />
</div>
);
};
render(<App />, document.getElementById("root"));
And the CSS:
.container{
display: flex;
align-items: center;
}
.border{
border-bottom: 1px solid black;
width: 100%;
}
.content {
padding: 0 10px 0 10px;
}
Update 29/03/2022
That's now possible with Material UI 🔥
https://mui.com/components/dividers/#dividers-with-text
You may want different spacing sometime
<Divider spacing={1}>Hello World</Divider>
Or
<Divider spacing={2}>Hello World</Divider>
For a configurable spacing here a Github Gist
Or a playground in codesandbox if you prefer
The current answer causes any text with spaces in-between to wrap:
If that happens, changing width: 100% to flexGrow: 1 should solve it:
border: {
borderBottom: "2px solid lightgray",
flexGrow: 1,
}
Unfortunately for now, having Divider with text on it in MUI is only available in v5, which is still in alpha stage. If you would like to try, you can download the alpha package, but be warned that it is still highly unstable and a lot of changes might be needed to migrate to v5, which is not very worth it.
GitHub discussion: https://github.com/mui-org/material-ui/issues/24036

Resources