How to convert a JSON style object to a CSS string? - javascript

I wanted to set my element's style as such:
this.refs.element.style = {
...this.props.style,
background: 'blue',
};
But apparently you can't use an object to set the ref's style. I have to use a CSS style string with ; separating the prop:values
I'm aware that most people would set style in the render function, but for performance reasons, I can't repeatedly re-render.

A performant answer is to map and join the Object.entries with semicolons:
const style = {
...this.props.style,
background: 'blue',
};
const styleString = (
Object.entries(style).map(([k, v]) => `${k}:${v}`).join(';')
);
It unwraps background:'blue', to background:blue; which works well for CSS
To replace any capital letter with dash lowercase letter
k = k.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);

this solution works in IE and handles camelCase keys like backgroundColor
const style = {
width: '1px',
height: '1px',
backgroundColor: 'red',
transform: 'rotateZ(45deg)',
}
const styleToString = (style) => {
return Object.keys(style).reduce((acc, key) => (
acc + key.split(/(?=[A-Z])/).join('-').toLowerCase() + ':' + style[key] + ';'
), '');
};
console.log(styleToString(style));
// output - "width:1px;height:1px;background-color:red;transform:rotateZ(45deg);"

Use https://www.npmjs.com/package/json-to-css. Note it will not add a semicolon to the last property to fix it you can beautify it with https://www.npmjs.com/package/cssbeautify
Example
const cssbeautify = require('cssbeautify')
const Css = require('json-to-css')
const json = {
"h1": {
"font-size": "18vw",
"color": "#f00"
},
".btn": {
"font-size": "18vw",
"color": "#f00"
}
}
const r = Css.of(json)
console.log(r)
const beautified = cssbeautify(r, {
autosemicolon: true
})
console.log(beautified)
Result
console.log src/utils/playground/index.spec.ts:22 // json-to-css
h1{font-size:18vw;color:#f00}
.btn{font-size:18vw;color:#f00}
console.log src/utils/playground/index.spec.ts:29 // cssbeautify
h1 {
font-size: 18vw;
color: #f00;
}
.btn {
font-size: 18vw;
color: #f00;
}

Adding to the great answer of #Artem Bochkarev
I'm adding a snippet to do the opposite conversion as well (string to object) which may come in handy to anyone stumbling here
const style = {
width: '1px',
height: '1px',
backgroundColor: 'red',
transform: 'rotateZ(45deg)',
};
const styleToString = (style) => {
return Object.keys(style).reduce((acc, key) => (
acc + key.split(/(?=[A-Z])/).join('-').toLowerCase() + ':' + style[key] + ';'
), '');
};
const stringToStyle = (style) => {
const styles = {};
style.split(';').forEach((s) => {
const parts = s.split(':', 2);
if (parts.length > 1) {
styles[parts[0].trim().replace(/-([a-z])/ig, (_, l) => l.toUpperCase())] = parts[1].trim();
}
});
return styles;
};
console.log(styleToString(style));
// output - "width:1px;height:1px;background-color:red;transform:rotateZ(45deg);"
console.log(stringToStyle(styleToString(style)));

TL;DR: The problem is that you are overwriting the entire "style" property of the element and losing its prototype and methods. You must add your style object without change the entire property. If you want to apply an object-like style to a DOM element, just do:
Object.assign(this.refs.element.style, {
background: 'blue',
color: 'white',
/** style properties:values goes here */
});
Explanation: The property "style" is an instance of the "CSSStyleDeclaration" interface. If you overwrite the interface it wont be a "CSSStyleDeclaration" anymore. It works when you set a string as value because javascript will pass the string directly to the element, without process anything.
CSSStyleDeclaration Reference Doc:
https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
If you want to do a test, go to your navigator > inspector > console and paste the code below:
const p1 = document.createElement('p');
p1.style = { color: 'blue' };
const p2 = document.createElement('p');
Object.assign(p2.style, { color: 'blue' });
console.log(p1);
console.log(p2);
The output will be:
<p style=""></p>
<p style="color: blue;"></p>

the css function in #material-ui/system can help you out
check more info here
import React from 'react';
import styled, { ThemeProvider } from 'styled-components';
import NoSsr from '#material-ui/core/NoSsr';
import { createMuiTheme } from '#material-ui/core/styles';
import { compose, spacing, palette, css } from '#material-ui/system';
const Box = styled.div`
${css(
compose(
spacing,
palette,
),
)}
`;
const theme = createMuiTheme();
export default function CssProp() {
return (
<NoSsr>
<ThemeProvider theme={theme}>
<Box color="white" css={{ bgcolor: 'palevioletred', p: 1, textTransform: 'uppercase' }}>
CssProp
</Box>
</ThemeProvider>
</NoSsr>
);
}

Related

Testing styled component with jest doesn't count css rules with className

I am testing my Text component with jest and jest-styled-components and toHaveStyleRule breaks my test with this error
"Value mismatch for property 'font-size'"
Expected
"font-size: 42px"
Received:
"font-size: 1.6rem"
This is my component.
const StyledText = styled.h1`
font-size: '1.6rem';
&.headline {
color: red;
&.headline-1 {
font-size: '42px';
}
}
`
const Text = ({h1}) => {
const className = h1 ? 'headline headline-1' : ' ';
return (
<StyledText className={className}>h1</StyledText>
)
}
And the test suit.
test("It should render correct heading when h1 is passed", () => {
const { getByRole } = render(<Text h1>h1</Text>);
const h1Styled = getByRole('heading', { level: 1 });
expect(h1Styled).toHaveClass('headline headline-1');
const baseHeadlineStyles = {
color: red,
};
expect(h1Styled).toHaveStyle(baseHeadlineStyles);
expect(h1Styled).toHaveStyleRule('font-size', '42px')
}
It seems like it only considers direct css rules on styled components.
ClassNames are applied properly, otherwise the below assertion would fail.
expect(h1Styled).toHaveClass('headline headline-1');
Interesting thing is that it works well with
expect(elem).toHaveStyle('font-size: 42px');
I need to use toHaveStyleRule as it also accepts media queries.

How to export variable from react component?

I am using Mantine for a search bar and I need to get the wordcount of the text area. This is using Nodejs and React. I need to be able to export this value to use in a different file.
import React, { useState } from 'react';
import { TextInput, createStyles } from '#mantine/core';
var count = document.getElementById('count');
const useStyles = createStyles((theme, { floating }: { floating: boolean }) => ({
root: {
position: 'relative',
},
label: {
position: 'absolute',
zIndex: 2,
top: 7,
left: theme.spacing.sm,
pointerEvents: 'none',
color: floating
? theme.colorScheme === 'dark'
? theme.white
: theme.black
: theme.colorScheme === 'dark'
? theme.colors.dark[3]
: theme.colors.gray[5],
transition: 'transform 150ms ease, color 150ms ease, font-size 150ms ease',
transform: floating ? `translate(-${theme.spacing.sm}px, -28px)` : 'none',
fontSize: floating ? theme.fontSizes.xs : theme.fontSizes.sm,
fontWeight: floating ? 500 : 400,
},
required: {
transition: 'opacity 150ms ease',
opacity: floating ? 1 : 0,
},
input: {
'&::placeholder': {
transition: 'color 150ms ease',
color: !floating ? 'transparent' : undefined,
},
},
}
)
);
export function FloatingLabelInput() {
const [focused, setFocused] = useState(false);
const [value, setValue] = useState('');
const { classes } = useStyles({ floating: value.trim().length !== 0 || focused });
const uniqueid = "input";
return(
<TextInput
id={ uniqueid }
placeholder="Add anything you want to the book of the internet."
required
classNames={classes}
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
mt="md"
onKeyUp={(e) => {
var text = value.split(' ');
var wordcount = 0;
for (var i = 0; i < text.length; i++) {
if (text[i] !== ' ') {
wordcount++;
}
}
count.innerText = wordcount;
}
}
autoComplete="nope"
/>
);
}
As you can see, it correctly outputs it into html, but returning inside the function doesnt work at all.
I tried exporting it, I tried returning it to the function but it doesn't see it. I tried exporting and using modules exports but that doesnt work either. Any help would be appreciated.
In the following code snippet, my root component (called App) is responsible for keeping the app state, but it can give any piece of state to any of its children. It can also give state modifiers (setX functions) to its children, which is what I am demonstrating here:
function Input ({ setWordCount }) {
function updateWordCount (event) {
setWordCount(event.target.value.split(' ').length)
}
return <input type="text" onKeyUp={updateWordCount} />
}
function SomeOtherComponent ({ count }) {
return (
<span>: {count} words</span>
)
}
function App () {
const [wordCount, setWordCount] = React.useState(0)
return (
<p>
<Input setWordCount={setWordCount} />
<SomeOtherComponent count={wordCount} />
</p>
)
}
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.5/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.5/umd/react-dom.production.min.js"></script>
<div id="app" />
As you can see, the Input component can call the setWordCount function provided by its parent to change a piece of state. Then, the parent (App) can give that piece of state to any number of its children. Each component can live in a separate file too, this would still work…
I'm not sure if I understood your question correctly, but hopefully, this can give you ideas you can reuse in your own code?

how can I use if-else statement inline in reactJS?

I've got this code from a tutorial video , but first of all I didn't get the purpose of clk function and how it is related to h1 tag and that trinary operator.
second , how can I use normal if-else instead of ternary operator and not only for adding class but changing its style too.
import React, {useState} from 'react';
import "./App.css";
function App(){
let [isRed,setRed] = useState(false);
function clk(){
setRed(true);
}
return(
<div>
<h1 className={isRed?"red":""}>Change My Color</h1>
<button onClick={clk}>ClickHere</button>
</div>
);
}
export default App;
You can do that by applying this
<h1 className={`${isRed ? "red" : ""}`}>Change My Color</h1>
Or
{
isRed ? (
<h1 className={"red"}>Change My Color</h1>
) : (
<h1 className={"other"}>Change My Color</h1>
)
}
Enclose your elements inside of a {} makes it interpreted as js code
`
{if(isRed) return < h1>...< /h1> else return < h1>...< /h1>}
`
should work.. Maybe you can use the same inside the class attribute, but it will be hard to read.
As for the click function, it is setting the value of isRed to true. This will create the reactive change to your style.
You can bind a memo to your <h1> element that gets calculated when that particular state changes. Please note that JSX is not JavaScript. It may appear similar, and you may be able to use 99% of the syntax, but there are differences.
You can learn more about memoized values here: React / Docs / Hooks / useMemo
const { useMemo, useState } = React;
const App = () => {
let [isRed, setRed] = useState(false);
let [isBlue, setBlue] = useState(false);
const onClickRed = (e) => setRed(!isRed); // onclick callback
const onClickBlue = (e) => setBlue(!isBlue); // onclick callback
const headerPropsRed = useMemo(() => {
console.log('Red updated!');
let props = {};
if (isRed) {
props = {
...props,
className: 'red',
style: {
...props.style,
fontStyle: 'italic'
}
}
}
return props;
}, [ isRed ]);
const headerPropsBlue = useMemo(() => {
console.log('Blue updated!');
let props = {};
if (isBlue) {
props = {
...props,
className: 'blue',
style: {
...props.style,
fontStyle: 'italic'
}
}
}
return props;
}, [ isBlue ]);
return (
<div>
<h1 {...headerPropsRed}>Change My Color</h1>
<button onClick={onClickRed}>Click Here</button>
<h1 {...headerPropsBlue}>Change My Color</h1>
<button onClick={onClickBlue}>Click Here</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('react'));
.as-console-wrapper { max-height: 3em !important; }
h1 { font-size: 1em; }
.red { background: red; }
.blue { background: blue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can use ternary operator working like this:
if(condition) ? "True part here" : "else part here (false part)"
Use case example :
const id = 1;
if(id === 1) ? "You are on right place" : "Sorry please check"
You can't use if-else in inline jsx but there exists some workarounds and you can choose whichever you want.
variant 1:
if(isRed){
const header = <h1 className='red' style={{ backgroundColor: 'red' ... }}>Change My Color</h1>
} else {
const header = <h1 style={{ backgroundColor: 'green' ... }}>Change My Color</h1>
}
return (
<div>
{header}
<button onClick={clk}>ClickHere</button>
</div>
);
variant 2: (but still ternary opeartor used)
if(isRed){
} else {
const header =
}
return (
<div>
{isRed ? (
<h1 className='red' style={{ backgroundColor: 'red' ... }}>Change My Color</h1>
) : (
<h1 style={{ backgroundColor: 'green' ... }}>Change My Color</h1>
)}
<button onClick={clk}>ClickHere</button>
</div>
);
There is no other way to replace ternary operator with if-else statement

Is it possible to test for custom data-attributes in css-in-js?

I am trying to reference data attributes from JSS, test for them, and apply the styles if the test is true. However, when I run the code below, it does not apply "color:green" and I have no idea why!
import React from "react";
import { createUseStyles } from "react-jss";
const useStyles = createUseStyles({
statusDiv: {
backgroundColor: "lightGrey",
padding: "10px",
fontWeight: "bold",
'[data-status="OPEN"]': {
// no effect ..?
color: "green",
},
},
});
const App = () => {
const classes = useStyles();
return (
<div data-status="OPEN" className={classes.statusDiv}>
OPEN
</div>
);
};
export default App;
That's an interesting case.
What you can do is:
export default {
'#global' : {
'div.testClass[data-custom="value"]': {
color: 'red',
border: '1px, solid navy'
}
}
}
This assumes a JSS plug-in #global installed, but this is something I think is quite useful to have.
More about the plug-in #global - here
And if you want to address a data- attribute like in your edited example 0 you should do:
testClass: {
'[data-custom="value"]': {
//styles for the custom data
}
}
And in the markup you'll have (depends on the way you extracting the class):
testClass.classes['[data-custom="value"]']
... styles for the custom data will be applied by the above.
The way you wrote in your last edit will not work. You should add a small bit though. To work the code example you provided:
...
fontWeight: "bold",
'& [data-status="OPEN"]': {
// no effect ..?
color: "green",
},
The & require a nesting plug-in setup - here is more about it.
Thanks to the amazing support from #Vladyn, the following code demonstrates this working perfectly :D
import React from "react";
import { create } from "jss";
import { JssProvider, createUseStyles } from "react-jss";
import nested from "jss-plugin-nested";
const jss = create();
jss.use(nested());
const useStyles = createUseStyles({
statusDiv: {
backgroundColor: "lightGrey",
padding: "10px",
fontWeight: "bold",
'&[data-status="OPEN"]': {
color: "green",
},
'&[data-status="CLOSED"]': {
color: "red",
},
},
});
const App = () => {
const classes = useStyles();
const status = "OPEN";
return (
<JssProvider jss={jss}>
<div data-status={status} className={classes.statusDiv}>
{status}
</div>
</JssProvider>
);
};
export default App;
If your JS looks like this:
<div data-customAttribute={value} className={classes.testClass}></div>
Your CSS will look like:
.testClass[data-customAttribute="value"] {
// your styles
}

How to execute and merge multiple functions result by using Lodash

Suppose that we have 2 different functions (or more) which accept an one argument from some executor and return the result object. Let me show in an example:
const style_1 = theme => ({
header : {
color : theme.primary
}
})
const style_2 = theme => ({
header : {
backgroundColor : theme.secondary,
color : "red"
}
})
I want to execute them and merge the resulted object into an one! In case of objects it's a trivial task eg.
const style_1 = {
header : {
color : theme.primary
}
}
const style_2 = {
header : {
backgroundColor : theme.secondary,
color : "red"
}
}
const res = {...style_1, ...style_2}
expected result is
{
header :
{
backgroundColor : "black",
color : "red"
}
}
But in case of functions it's becomes a lil bit annoying.
So question is. Is it possible to implement what I want? (via Lodash or by using something else)
I've decided that I might create something like that
const style_1 = theme => ({
header : {
color : theme.secondary
backgroundColor : "black"
},
footer : {
color : "purple"
}
})
const style_2 = (theme, extendStyles) => {
const extendStyles = extendStyles(theme);
return {
header : {
color : theme.primary,
...extendsStyles.header
},
...extendStyles
}
}
but this solutions is a lil bit ugly cause I should take care about this thing in an every style which might be override. Maybe there are exist some utilities/helpers which can help to handle it in a more nice way?
P.S. Don't ask me about this interesting requirements, it's just a withStyle feature of the MaterilUI and I wondered how to handle it right.
Thanks for any help.
You can use _.invokeMap() and _.merge() to generate a combine styled object from an array of style functions and a theme:
const applyTheme = (styles, theme) =>
_.merge(..._.invokeMap(styles, _.call, null, theme));
const style_1 = theme => ({
header : {
color : theme.primary,
border: `1px solid ${theme.primary}`
}
})
const style_2 = theme => ({
header : {
backgroundColor : theme.secondary,
color : "red"
}
})
const theme = { primary: 'red', secondary: 'blue' }
const result = applyTheme([style_1, style_2], theme)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

Categories

Resources