Using Styled Components within a NPM Module - javascript

I am unsure if this is a me issue or the libary or just not possible in general (although it should).
What I am trying to do is the following:
I have used Material UI to create a "Libary" that I can reuse in all my projects so I do not have to do all the things every time that I usually do. I have used styled components to style some of the material ui components such as Button and TextField. I have exposed some of those modified properties to the app that consumes my libary. When these styles that are exposed are generic everything works fine.
As an example here is my modified Button with just basic styles:
const StyledButton = styled(Button)`
width: ${props => props.width || "265px"};
height: ${props => props.height || "40px"};
margin: ${props => props.margin} !important;
padding: ${props => props.padding};
font-weight: ${props => props.fontWeight || "700"};
text-decoration: ${props => props.textDecoration};
`
This button then is used in the libaries Button component thus masking the material ui part of it like so:
const MyButton = (props) => {
return (<StyledButton width={props.width} height={props.height} .../>)
}
And so forth. This is working. My issue arrises when I am trying to make the button change on some mobile breakpoint.
This is my full button component here:
const StyledButton = styled(Button)`
width: ${props => props.width || "265px"};
height: ${props => props.height || "40px"};
margin: ${props => props.margin} !important;
padding: ${props => props.padding};
font-weight: ${props => props.fontWeight || "700"};
text-decoration: ${props => props.textDecoration};
#media only screen and (max-width: ${MOBILE_BREAKPOINT}) {
width: ${props => props.mWidth || "165px"};
height: ${props => props.mHeight || "40px"};
margin: ${props => props.mMargin};
padding: ${props => props.mPadding};
font-weight: ${props => props.mFontWeight};
text-decoration: ${props => props.mTextDecoration};
}
`;
However when I am trying to now set these values I get errors in the console from React (as expected) that are not hindering the execution of my code. But still nothing is happening when I decrease screen width below 450px (MOBILE_BREAKPOINT).
The way I am trying to do it is by deconstructing the props js const {mWidth, mHeight} = mobile;
The values show up in the props of the component but they are not used. I am passing them like this:
<MyButton width={"75%"} height={"40px"} mobile={{mWidth: "95%}}/>
It just does not do what I want it to when the breakpoint is passed. The width stays at 75%. 95% is never activated.
Can anyone tell me what I am doing wrong or if this is impossible to achieve?
I appreaciate the help!

You can simply write your MyButton code like this:
const MyButton = (props) => {
const { width, height, mobile: {mWidth, mHeight}} = props;
return (
<StyledButton
width={width}
height={height}
mWidth={mWidth}
mHeight={mHeight}
/>
);
}
And then your media query worked fine.
I recommend you to set default value in destructuring part in the MyButton instead of in the StyledButton, in order to do that you can change MyButton component like this:
const MyButton = (props) => {
const {
width = "265px",
height = "40px",
mobile: {mWidth = "165px", mHeight = "40px"}
} = props;
return (
<StyledButton
width={width}
height={height}
mWidth={mWidth}
mHeight={mHeight}
/>
);
}
and then StyledButton should change like this:
const StyledButton = styled(Button)`
width: ${props => props.width};
height: ${props => props.height};
#media only screen and (max-width: ${MOBILE_BREAKPOINT}) {
width: ${props => props.mWidth};
height: ${props => props.mHeight};
}
`;
and you also can refactor StyledButton like below:
const StyledButton = styled(Button)`
width: ${({ width }) => width};
height: ${({ height }) => height};
#media only screen and (max-width: ${MOBILE_BREAKPOINT}) {
width: ${({ mWidth })=> mWidth};
height: ${({ mHeight })=> mHeight};
}
`;

Related

findByType function not finding property?

I'm writing tests for components I have main component Icon
const Icon: React.FC<IIconPropTypes> = ({ size, color, icon, onClick, blockEvents }) => (
<IconWrapper onClick={onClick} blockEvents={blockEvents}>
<IconSvg color={color} hasClick={!!onClick} width={size}>
<path d={ICONS_PATH[icon]} />
</IconSvg>
</IconWrapper>
);
and IconSvg which I am testing
const IconSvg = styled.svg<IIconSvgProps>`
cursor: ${(props) => (props.hasClick ? 'pointer' : 'default')};
display: inline-block;
vertical-align: middle;
width: ${(props) => (props.width ? `${props.width}px` : '100%')};
path {
fill: ${(props) => (props.color ? props.color : '#edcd7d')};
transition: all 0.3s;
}
`;
This is how my test looks like
it('matches snapshot and renders without color prop', () => {
const [hasClick, width] = [true, 100];
const component = renderer.create(<IconSvg hasClick={hasClick} width={width}/>);
const tree = component.toJSON() as ReactTestRendererJSON;
expect(tree).toMatchSnapshot();
expect(component.root.findByType('path')).toHaveStyleRule('fill', '#edcd7d');
})
But findByType cannot find the path, and therefore the fill property, can anyone know what the error is and how to reach it?
You are testing your IconSvg component here:
let component = renderer.create(<IconSvg hasClick={hasClick} width={width}/>);
According to your code IconSvg is just a styled svg. You need to test Icon component which has a path element in it:
let component = renderer.create(<Icon size={} color={} icon={} onClick={} blockEvents={} } />);

Show hover on specific card in react

I am facing this problem. I want to show hover box on this box which is chosen. F.e when I hover on Box One I want to show Hover One, Box Two -> Hover Two. But in my example when I hover on One both are displayed. I am trying to do this with refs or e.target but always something is not as I want.
Link to stackblitz: https://stackblitz.com/edit/react-hc4741?file=src/App.js
import React, { useState } from "react";
import "./style.css";
import { BooksSection, BookCard, BookCardHover } from "./Styled";
export default function App() {
const [displayBookCardHover, setDisplayBookCardHover] = useState(false);
const showCardHover = () => {
setDisplayBookCardHover(true);
};
const hiddenCardHover = () => {
setDisplayBookCardHover(false);
};
return (
<div>
<BooksSection>
<BookCard
bgColor={"#000"}
color={"#fff"}
onMouseEnter={showCardHover}
onMouseLeave={hiddenCardHover}
>
<BookCardHover display={displayBookCardHover}>
Hover One
</BookCardHover>
Box One
</BookCard>
<BookCard
bgColor={"#fff"}
color={"#000"}
onMouseEnter={showCardHover}
onMouseLeave={hiddenCardHover}
>
<BookCardHover display={displayBookCardHover}>
Hover Two
</BookCardHover>
Box Two
</BookCard>
</BooksSection>
</div>
);
}
styled components
import styled from "styled-components";
export const BooksSection = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100wh;
`;
export const BookCard = styled.div`
width: 50%;
height: 500px;
padding: 20px 0;
background: ${props => props.bgColor};
color: ${props => props.color};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
`;
export const BookCardHover = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 50px;
background: rgba(0, 0, 0, 0.7);
visibility: ${({ display }) => (display ? "100" : "hidden")};
`;
The problem is you have the exact same component with the exact same prop value in both places, so they will be shown/hidden at the same time no matter what you do to the displayBookCardHover value.
The trick is to use a separate value for each. Like this:
const [hoverIndex, setHoverIndex] = useState(-1);
...
const showCardHover = (index) => {
setHoverIndex(index);
}
const hiddenCardHover = () => {
setHoverIndex(-1);
}
...
<BookCard
...
onMouseEnter={() => showCardHover(0)}
...
>
<BookCardHover display={hoverIndex === 0}>
...
<BookCardHover display={hoverIndex === 1}>
Hope you get the idea.
On a side note, there's no "100" value for visibility prop. It's either "hidden" or "visible".
import React, { useState } from "react";
import "./style.css";
import { BooksSection, BookCard, BookCardHover } from "./Styled";
export default function App() {
const [displayBookCardHover, setDisplayBookCardHover] = useState({
boxOneHover: false,
boxTowHover: false
});
const showCardHover = box => {
if (box === 1) {
setDisplayBookCardHover(ps=>({ ...ps, boxOneHover: true }));
} else {
setDisplayBookCardHover(ps=>({ ...ps, boxTowHover: true }));
}
};
const hiddenCardHover = box => {
if (box === 1) {
setDisplayBookCardHover(ps=>({ ...ps, boxOneHover: false }));
} else {
setDisplayBookCardHover(ps=>({ ...ps, boxTowHover: false }));
}
};
return (
<div>
<BooksSection>
<BookCard
bgColor={"#000"}
color={"#fff"}
onMouseEnter={() => showCardHover(1)}
onMouseLeave={() => hiddenCardHover(1)}
>
<BookCardHover display={displayBookCardHover.boxOneHover}>
Hover One
</BookCardHover>
Box One
</BookCard>
<BookCard
bgColor={"#fff"}
color={"#000"}
onMouseEnter={() => showCardHover(2)}
onMouseLeave={() => hiddenCardHover(2)}
>
<BookCardHover display={displayBookCardHover.boxTowHover}>
Hover Two
</BookCardHover>
Box Two
</BookCard>
</BooksSection>
</div>
);
}
I think that the BookCard should be a component. Each one should have its own state. In App.js you can use BookCard and pass bgColor and color and whatever you want to customize each BookCard as props and use them in it.
The issue with your code is - both of the BookCardHover component base their state of the display using the same reference state displayBookCardHover, so, when one changes the value of displayBookCardHover, it automatically reflect on the other. I would recommend the approch suggested by #technophyle to seperate them.

Nesting styled-components 'Atoms' together in 'Molecules' -> Styling / Positioning

I need to create a React Component Library
For this i use React with Typescript and styled-components.
I got stuck when i try to reuse my Atoms in a Molecule...
I create my Button Atom Component like this:
interface IButtonProps {
text?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
}
const StyledButton = styled.button<IButtonProps>`
height: 70px;
width: 70px;
border: none;
border-radius: 5px;
background-color: #88BDBC;
color: #254E58;
font-family: Alata, sans-serif;
font-size: 35px;
transition: all 350ms;
&:hover {
background-color: #254E58;
color: #88BDBC;
}
&:active {
background-color: #112D32;
color: #88BDBC;
}
`;
const Button = ({
text = "",
onClick
}: IButtonProps): React.ReactElement => {
return (
<StyledButton onClick={onClick}>{text}</StyledButton>
);
};
This is only a really not perfect Example for my Atoms. I create all Atoms like this. I only define the Style inside the Atoms -> Colors, Borders, Margins and so on. Not the Styles outside -> For Example Padding then i think its depends much in which context this Button is used.
So I'm very encapsulated.
When i want to use the Button in the for example 'Controller' Molecule then i need to give that Button some Positions and maybe some Paddings etc.
But in my Parent Styled Component i doesn't now how to Point them correctly. Especially when i have some bigger mix of Components like here.
interface IControllerProps {
text1: string;
text2: string;
text3: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>
}
const StyledController = styled.div<IControllerProps>`
/* ... */
`;
const Controller = ({
onClick
}: IControllerProps) => {
const [powerOn, setPowerOn] = React.useState(false);
const [bankType, setBankType] = React.useState(false);
const [volume, setVolume] = React.useState(50);
return (
<StyledController>
<Text text="Hello"/>
<Switch checked={powerOn} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {setPowerOn(e.target.checked)}}/>
<Button onClick={() => false}/>
<Text text="Hello"/>
<RangeSlider value={volume} min={0} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setVolume(parseInt(e.target.value))} />
<Text text="Hello"/>
<Switch checked={bankType} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {setBankType(e.target.checked)}}/>
<Button onClick={onClick}/>
<Button onClick={onClick}/>
</StyledController>
)
};
Do i need to work with Propertys to give that to the Button ... Point everything with pseudoclasses ... Define everything in the Button (But what is with flexibility to Context of the reusable Button) ...
What is the best practice for that?
You target the styles like so (I answer in javascript for less complexity):
// Button.react.js
export const StyledButton = styled.button`
background-color: #88bdbc;
`;
// Controller.react.js
import { StyledButton } from './Button.react.js';
const StyledController = styled.div`
${StyledButton} {
background-color: blue;
}
`;
Please check the related docs.
In additon of the Answer from Dennis Vash:
For my Example Button Component this means i need to change it like this:
interface IButtonProps {
text?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
className?: string
}
const UnstyledButton = ({
text = "",
onClick,
className
}: IButtonProps): React.ReactElement => {
return (
<button onClick={onClick} className={className}>{text}</button>
);
};
const Button = styled(UnstyledButton)<IButtonProps>`
height: 70px;
width: 70px;
border: none;
border-radius: 5px;
background-color: #88BDBC;
color: #254E58;
font-family: Alata, sans-serif;
font-size: 35px;
transition: all 350ms;
&:hover {
background-color: #254E58;
color: #88BDBC;
}
&:active {
background-color: #112D32;
color: #88BDBC;
}
`;
The Reason for that is: This only works in the Context of styled-components and not on Context of ReactElement.
Important Point from styled-component docs:
However, wrapping in a styled() factory makes it eligible for interpolation -- just make sure the wrapped component passes along className.
When u use the Button directly from Styled Component (like Dannis Vash mentioned) then u can refer to it directly.

React — Passing props with styled-components

I just read in the styled-components documentation that the following is wrong and it will affect render times. If that is the case, how can I refactor the code and use the required props to create a dynamic style?
Thank you in advance.
Tab component
import React from 'react'
import styled from 'styled-components'
const Tab = ({ onClick, isSelected, children }) => {
const TabWrapper = styled.li`
display: flex;
align-items: center;
justify-content: center;
padding: 100px;
margin: 1px;
font-size: 3em;
color: ${props => (isSelected ? `white` : `black`)};
background-color: ${props => (isSelected ? `black` : `#C4C4C4`)};
cursor: ${props => (isSelected ? 'default' : `pointer`)};
`
return <TabWrapper onClick={onClick}>{children}</TabWrapper>
}
export default Tab
I believe what the documentation is saying is that you should avoid including your styles inside of the rendering component:
DO THIS
const StyledWrapper = styled.div`
/* ... */
`
const Wrapper = ({ message }) => {
return <StyledWrapper>{message}</StyledWrapper>
}
INSTEAD OF THIS
const Wrapper = ({ message }) => {
// WARNING: THIS IS VERY VERY BAD AND SLOW, DO NOT DO THIS!!!
const StyledWrapper = styled.div`
/* ... */
`
return <StyledWrapper>{message}</StyledWrapper>
}
Because what happens is when the component's Props changes, then the component will re-render and the style will regenerate. Therefore it makes sense to keep it separate.
So if you read further on to the Adapting based on props section, they explain this:
const Button = styled.button`
/* Adapt the colours based on primary prop */
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
// class X extends React.Component {
// ...
render(
<div>
<Button>Normal</Button>
<Button primary>Primary</Button>
</div>
);
// }
this works because when you use the Button component in class X, it will know the props of class X without you having to tell it anything.
For your scenario, I imagine the solution would be simply:
const TabWrapper = styled.li`
display: flex;
align-items: center;
justify-content: center;
padding: 100px;
margin: 1px;
font-size: 3em;
color: ${props => (props.isSelected ? `white` : `black`)};
background-color: ${props => (props.isSelected ? `black` : `#C4C4C4`)};
cursor: ${props => (props.isSelected ? 'default' : `pointer`)};
`;
const Tab = ({ onClick, isSelected, children }) => {
return <TabWrapper onClick={onClick}>{children}</TabWrapper>
}
const X = <Tab onClick={() => console.log('clicked')} isSelected>Some Children</Tab>
I haven't tested this at all, so please feel free to try it out and let me know if it works for you or whatever worked for you!
You can pass an argument with Typescript as follows:
<StyledPaper open={open} />
...
const StyledPaper = styled(Paper)<{ open: boolean }>`
top: ${p => (p.open ? 0 : 100)}%;
`;
Another way to do it would be
const StyledDiv = styled.div.attrs((props: {color: string}) => props)`
width: 100%;
height: 100%;
background-color: ${(props) => props.color};
`
//...
render() {
return (
<StyledDiv color="black">content...</StyledDiv>
);
}
This way you are type-safe in terms of the props you want to send into the styled component. (Good when coding in Typescript)
For a more simple example with functional components:
Suppose you have an arrow like polygon and you need 2 of them pointing in different directions. So you can pass the rotate value by props
<Arrow rotates='none'/>
<Arrow rotates='180deg'/>
Then in the Component Arrow you have to pass the props like normal component to the styled component but in the styled component you have to use it like props:
import React from 'react';
import styled from "#emotion/styled";
const ArrowStyled = styled.div`
background-color: rgba(255,255,255,0.9);
width: 24px;
height: 30px;
clip-path: polygon(56% 40%,40% 50%,55% 63%,55% 93%,0% 50%,56% 9%);
transform: rotate(${props => props.rotates});
`
const Arrow = ({rotates}) => {
return (
<ArrowStyled rotates={rotates}/>
);
}
export default Arrow;
If you're using Typescript create an interface inside your styles file!
Otherwise, you won't be able to access props in your CSS
import styled from 'styled-components'
interface StyledLiProps{
selected: boolean
}
export const TabWrapper = styled.li`
// styles ...
color: ${props => (selected ? `white` : `black`)};
background-color: ${props => (selected ? `black` : `#C4C4C4`)};
`
And don`t forget to declare the props you want to use in your CSS inside your JSX
interface TabProps{
text: string;
}
const Tab = ({ text }: TabProps) => {
//...
return <TabWrapper selected={isSelected} onClick={() => updateTab}>{text}</TabWrapper>
}
Consider styled components documentation gives example of using reacts context api [2] for different themes.
[1] https://www.styled-components.com/docs/advanced
[2] https://reactjs.org/docs/context.html
Exporting styled-component
Button
and passing scrollPosition as a prop in functional component
PassingPropsToSyledComponent
import styledComponents from "styled-components";
export const Button = styledComponents.div`
position: ${ props => props.scrollPosition ? 'relative' : 'static' };
`;
export const PassingPropsToSyledComponent = ()=> {
return(
<Button scrollPosition={scrollPosition}>
Your Text Here
</Button>
)
}

React - Prop is undefined in stateless function

I am passing props from one stateless function to another, and I get a reference error saying the prop is undefined. Here is the parent function:
const Home = () => {
return (
<App>
<BackgroundImage url="mercedes-car.jpg">
<h1>Test</h1>
</BackgroundImage>
</App>
)
}
And here is the BackgroundImage function:
const Image = styled.div`
background-image: ${props => url(props.imageUrl)};
background-size: cover;
height: 100%;
width: 100%;
`;
const BackgroundImage = (props) => {
console.log(props)
return (
<Image imageUrl={ props.url }>
{ props.children }
</Image>
)
}
The error is that url is undefined; however, when I console.log(props), I get an object with url and children. Any direction or explanation as to why this error is throwing would be appreciated!
I'm guessing you meant
background-image: ${props => url(props.imageUrl)};
to be
background-image: url(${props => props.imageUrl});
since the result of that function needs to be a string. Otherwise you're trying to call a function called url.
you have a scope issue. change it to:
const BackgroundImage = (props) => {
console.log(props)
const Image = styled.div`
background-image: url(${props.url});
background-size: cover;
height: 100%;
width: 100%;
`;
return (
<Image>
{ props.children }
</Image>
)
}
basically the props are not available to your image, because styled.div is not a normal react component that has props.
Another way is to leave your code as is but set the background image from inside the return function:(and remove it from the styled.div)
return (
<Image style={{backgroundImage: `url(${props.url})`}}>
{ props.children }
</Image>
)
}

Categories