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.
Related
I have component within I containment other components. In browser It looks like good but during unit testing I'm getting below an error. I don't know why. I have try render with .map() method but it didn't work. Thanks for help!
Error: Uncaught [Invariant Violation: Objects are not valid as a React child (found
: object with keys {$$typeof, render, attrs, componentStyle, displayName, foldedComponentIds, styledComponentId, target, withComponent, warnTooManyClasses, toString}). If you meant to render a collection of children, use an array instead.
AppBarTop.js
const AppBarTopWrapper = styled.div`
background: ${props => props.theme.color.backgroundSecondary};
border-bottom: 1px solid ${props => props.theme.color.line};
padding: 2rem 2rem 0rem 2rem;
color: ${props => props.theme.color.secondaryText};
`
const AppBarTop = ({ children }) => (
<AppBarTopWrapper>{children}</AppBarTopWrapper>
)
AppBarTop.propTypes = {
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}
export default AppBarTop
Test
const Head = styled.div
function renderTitle(props) {
return renderWithRouter(
<AppBarTop>
<Head>
<UpwardButton level={2} />
<Title level={1} {...props.message} />
</Head>
</AppBarTop>,
{
withStore: true,
},
)
}
const testFormattedMessage = text => ({
id: 'test',
defaultMessage: text,
})
describe('<Heading />', () => {
it('Expect to not log errors in console', () => {
const spy = jest.spyOn(global.console, 'error')
console.log(renderTitle({ message: testFormattedMessage('test title') }))
renderTitle({ message: testFormattedMessage('test title') })
expect(spy).not.toHaveBeenCalled()
})It looks like your post is mostly code; please add some more details.
})
styled.div returns a template function rather than a component. Hence in your code
const Head = styled.div
function renderTitle(props) {
return renderWithRouter(
<AppBarTop>
<Head>
<UpwardButton level={2} />
<Title level={1} {...props.message} />
</Head>
</AppBarTop>,
{
withStore: true,
},
)
}
Head is not a component (but a regular function), i.e. it's not a valid React child. Replacing <Head> with <div>, or invoking template function with empty string: const Head = styled.div`` would be sufficient.
I want to check if aria-expanded changes after a button click. This is my component
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { ArrowTemplate } from './ArrowTemplate';
import { colors } from '../../utils/css';
import { Text } from '../Text';
const Accordion = ({
rtl, content, color, title, className,
}) => {
const element = useRef(null);
const [isAccordionExpanded, setIsAccordionExpanded] = useState(false);
const toggleAccordion = () => {
setIsAccordionExpanded(!isAccordionExpanded);
};
const height = element.current ? element.current.scrollHeight : '0';
return (
<div className={`accordion-section ${className}`}>
<button className={'accordion-btn'} onClick={toggleAccordion}>
<p className={'accordion-title'}>
<Text isRtl={rtl}>{title}</Text>
</p>
<ArrowTemplate
direction={isAccordionExpanded ? 'up' : 'down'}
onClick={toggleAccordion}
rtl={rtl}
color={color}
/>
</button>
<AccordionContent
className={'accordion-content'}
height={height}
isAccordionExpanded={isAccordionExpanded}
ref={element}
aria-expanded={isAccordionExpanded}
>
<div className={'accordion-text'}>
<Text isRtl={rtl}>{content}</Text>
</div>
</AccordionContent>
</div>
);
};
export const StyledAccordion = styled(Accordion)`
font-family: "Open Sans", sans-serif;
text-align: ${({ rtl }) => (rtl ? 'right' : 'left')};
display: flex;
flex-direction: column;
.accordion-btn {
position: relative;
width: 100%;
background-color: ${colors.LG_GREY_4};
color: ${colors.LG_GREY_5};
cursor: pointer;
padding: 40px 18px;
display: flex;
align-items: center;
border: none;
outline: none;
:hover,
:focus,
:active {
background-color: ${colors.LG_GREY_6};
}
.accordion-title {
${({ rtl }) => (rtl ? 'right: 50px;' : 'left: 50px;')};
position: absolute;
font-weight: 600;
font-size: 14px;
}
}
`;
export const AccordionContent = styled.div`
max-height: ${({ isAccordionExpanded, height }) => (isAccordionExpanded ? height : '0')}px;
overflow: hidden;
background: ${colors.LG_GREY_7};
transition: max-height 0.7s;
.accordion-text {
font-weight: 400;
font-size: 14px;
padding: 18px;
}
`;
Here are my tests
import React from 'react';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { StyledAccordion, AccordionContent } from '../Accordion';
configure({ adapter: new Adapter() });
describe('<StyledAccordion/>',
() => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<StyledAccordion/>);
});
it('should match the snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should originally have aria-expanded set to false', () => {
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
});
it('should set aria-expanded to true onClick', () => {
wrapper.find('.accordion-btn').simulate('click');
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
});
});
And here's what I'm getting in the console
FAIL src/components/Accordion/test/Accordion.test.js (21.497s)
● <StyledAccordion/> › should originally have aria-expanded set to false
Method “props” is meant to be run on 1 node. 0 found instead.
16 | });
17 | it('should originally have aria-expanded set to false', () => {
> 18 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
| ^
19 | });
20 | it('should set aria-expanded to true onClick', () => {
21 | wrapper.find('.accordion-btn').simulate('click');
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as props] (node_modules/enzyme/src/ShallowWrapper.js:1160:17)
at Object.props (src/components/Accordion/test/Accordion.test.js:18:45)
● <StyledAccordion/> › should set aria-expanded to true onClick
Method “simulate” is meant to be run on 1 node. 0 found instead.
19 | });
20 | it('should set aria-expanded to true onClick', () => {
> 21 | wrapper.find('.accordion-btn').simulate('click');
| ^
22 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
23 | });
24 | });
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as simulate] (node_modules/enzyme/src/ShallowWrapper.js:1118:17)
at Object.simulate (src/components/Accordion/test/Accordion.test.js:21:38)
How can I test attributes?
Checking attribute by verifying props is fine. Simulating click is fine.
The only cause test fails is how shallow() works under the hood.
You may figure out that by yourselves by checking what wrapper.debug() returns(say by adding console.log(wrapper.debug()))
You will see something like <StyledComponentConsumer><Component ...></StyledComponentConsumer>. So the reason is shallow() does not render nested component. Say, if there were no styled-components and you tried to shallow(<Accordion />).find('span')(assuming that <Text> should be rendered as <span>) you will never find that as well.
First solution may be using mount() instead of shallow()(you should not even need to change test). But I would like to go different way(just an opinion though: https://hackernoon.com/why-i-always-use-shallow-rendering-a3a50da60942)
Read more about shallow vs mount difference at https://medium.com/#Yohanna/difference-between-enzymes-rendering-methods-f82108f49084
Another approach is to use .dive() like
wrapper.dive().find(...)
or even at initialization time:
const wrapper = shallow(...).dive();
And finally you may just export two versions: base one(and write tests against that) and wrapped into styled-components theming. Needs slightly more changes in code. And dislike approaches above tests will not require updates each time you add more HOC wrappers(say wrapping component into styled-components, then connect to redux and finally adding withRouter - tests will still work the same this way). But from the other side this approach does not test your component as it's actually integrated so I'm not really sure if this approach is better.
How could I recreate the following css in a react style css module?
.thing1, .thing2, .thing3, .thing4 { color: red }
.thing5, .thing6, .thing7 { color: blue }
Essentially I receive a prop containing a number/string which are grouped to the same css. But im not sure how I would recreate this setup in javascript. The below seems a bit silly to have to do. I would also like If i could avoid doing something like a array look up
const styles = {
thing1:{ color:red },
thing2:{ color:red },
thing3:{ color:red },
thing4:{ color:red },
thing5:{ color:blue },
thing6:{ color:blue },
thing7:{ color:blue },
}
Thoughts?
Edit
So an example would be:
render() {
let {classes} = this.props
return (
<div className={??? + this.props.input}></div>
)
}
if going the css route I would do
render() {
let {classes} = this.props
return (
<div className={"thing" + this.props.input}></div>
)
}
I made a helper to merge the styles, here is it:
const mergeStyles = styles => styles.join(' ');
export default mergeStyles;
And i use it this way:
<div className={mergeStyles([styles.boxInfo, styles.narrowBoxInfo])}>
If the styles are
.boxInfo {}
.narrowBoxInfo {}
Hope it helps!
In the Contentful CMS, I have two different content-types: BigCaseStudy and BigCaseStudySection. To get this content to appear in my Gatsby 2.x site, my thinking was:
Do query 1, which gets all the BigCaseStudy fields I want to display, and also contains the content's ID field as metadata.
Take that ID from query 1, match to a Contentful reference field (which contains an ID) in query 2
Do query 2, return all matching BigCaseStudySection fields
The end goal would be to display the original BigCaseStudy with all of the BigCaseStudySection (usually numbering between 3-5 of them). You can look at my queries to see the fields, there are bunch.
I think some combination of GraphQL variables and queries would get me there (maybe a mutation)? It's not clear and I haven't seen any complex examples of querying one set of stuff and then using the response to make another call, like chained promises or async/await in JS.
Any ideas on the right construction?
bigcasestudy.js component with GraphQL queries:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
#media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Setup destructured references to each Contentful object passed in through the GraphQL call
const { caseStudyTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudySubtitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntroTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntro } = this.props.data.contentfulBigCaseStudy.caseStudyIntro
const { caseStudyLink } = this.props.data.contentfulBigCaseStudy
console.log(this)
return (
<Layout>
<HeroContainer
bgImgSrc={ this.props.data.contentfulBigCaseStudy.caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
},
contentfulBigCaseStudySection (id: $postId) {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
`
gatsby-node.js file:
/**
* Implement Gatsby's Node APIs in this file.
*
* ######################################################
* BIG CASE STUDY BACKEND CODE
* ######################################################
*
* We are using the .createPages part of the Gatsby Node API: https://next.gatsbyjs.org/docs/node-apis/#createPages
* What this does is dynamically create pages (surprise) based on the data you feed into it
*
* Feed the contentful API call into the promise
* Here I'm calling BigCaseStudy, which is a custom content type set up in contentful
* This is briefly explained over here: https://www.gatsbyjs.org/packages/gatsby-source-contentful/
*
* Also, note the caseStudyIntro field is long text `markdown`
* Gatsby returns the long text field as an object
* Calling it's name inside of the object returns the HTML
* Read more here: https://github.com/gatsbyjs/gatsby/issues/3205
*/
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
const caseStudySections = result.data.allContentfulBigCaseStudySection.edges
let caseStudySectionMatches = caseStudySections.filter(
caseStudySection => caseStudySection.bigCaseStudyReference.id === caseStudy.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: caseStudySectionMatches.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}
I ran into this challenge too and couldn't find a good solution to accomplish that (although I wasn't using Contentful), but I did work past it and think I can help. You'll need to shift your thinking a bit.
Basically, GraphQL isn't really meant to query for the data you need to run another query. It's more of a 'ask for what you need' sort of tool. GraphQL wants to run a single query for exactly what you need.
The parameter you need for your query actually comes from your gatsby-node.js file. Specifically, the context property of createPages()(a Node API that gatsby makes available).
Is that enough to get you pointed in the right direction? If you need a bit more of a hand, then there are two things I need to know:
1. A little more context around what you're trying to accomplish. What is the specific data you want available to the end user?
2. What your gatsby-node.js file looks like.
Short answer: you don't do callbacks with GraphQL. You do one query that gets everything you need all at once.
Longer answer: I had to reconstruct how the gatsby-node.js file fetched Contentful content and then filtered through it. In Gatsby, you want to set up the queries in gatsby-node.js to go fetch everything from your data source because it's a static site generator. It's architecture brings all that data in and then parses it out accordingly.
The GraphQL query from my original question was fine. I changed .then() of the promise to use .filter() on my results, comparing the relationship field of the child nodes to the id of the parent nodes.
gatsby-node.js:
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
let matchedCaseStudySections = result.data.allContentfulBigCaseStudySection.edges.filter(
caseStudySection =>
caseStudySection.node.bigCaseStudyReference.id === caseStudy.node.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: matchedCaseStudySections.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}
Once you've set this up, the createPage part of Gatsby Node API sends the parent and all of it's nodes over to the component param you set.
Inside of my component, I can now make a GraphQL query for all children nodes. That now returns what I want and conforms to the idea that GraphQL makes one request instead of multiple like I was trying to do. The only tricky part is that you have to use .map() in the render part of the component to loop over all the child nodes sent back from Contentful.
bigcasestudy.js component:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
#media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Destructure Case Study Intro stuff
const {
caseStudyHero,
caseStudyIntro,
caseStudyIntroTitle,
caseStudyLink,
caseStudySubtitle,
caseStudyTitle
} = this.props.data.contentfulBigCaseStudy
// Setup references to Case Study Sections, destructure BigCaseStudySection object
const caseStudySections = this.props.data.allContentfulBigCaseStudySection.edges.map(
(currentSection) => {
return currentSection.node
}
)
// Case Study Section can be in any order, so we need to sort them out
const caseStudySectionsSorted = caseStudySections.sort( (firstItem, secondItem) => {
return firstItem.order > secondItem.order ? 1 : -1
})
console.log(caseStudySectionsSorted)
return (
<Layout>
<HeroContainer
bgImgSrc={ caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro.caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
{
caseStudySectionsSorted.map( (caseStudySection, index) => {
return <IntroTitle key={ index }>{ caseStudySection.title }</IntroTitle>
})
}
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`
H/t: thanks to #taylor-krusen for rearranging how I was approaching this problem.
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>
);
}