React: Styled-components loading button - button

I am using React- typescript for my app. For Styling I am using Styled-components. I have created one global button component. Inside the button component i used loader. My target is when user will click the button it will display loading. From the parent component I have created one fake api call and in there I added settime-out 5s. But when I click the button it does not display the loader which was in Button component. From the parent component set-time out works and it display in my console. I don't know why it does not the display the loading ..... Also i added the disabled option when fake api will call button should be disabled. That logic also does not work.
Here is the Button component
import * as React from "react";
import styled, { css } from "styled-components";
interface IButtonProps {
children?: React.ReactChild;
className?: string;
size?: "small" | "medium" | "big";
themes?: "primary" | "secondary" | "dark" | "light";
disabled?: boolean;
loading?: boolean;
style?: React.CSSProperties;
onClick?: () => void;
onSubmit?: () => void;
}
const Button = ({
children,
className,
size,
themes,
disabled,
loading,
style,
onClick,
onSubmit
}: IButtonProps) => (
<button
className={className}
onClick={onClick}
onSubmit={onSubmit}
style={
disabled && disabled
? { opacity: 0.5, pointerEvents: `none` }
: loading ? { ...style, pointerEvents: `none` } : //This is my disabled condition.
style
}
>
{loading ? <p>loading...</p> : children} //This is my loading condition.
</button>
);
const sizes = {
small: css`
padding: 5px 20px;
font-size: 12px;
`,
medium: css`
padding: 10px 30px;
font-size: 14px;
`,
big: css`
padding: 15px 40px;
font-size: 18px;
`
};
const ButtonThemes = {
primary: css`
border: 1px solid tomato;
background: tomato;
color: white;
`,
secondary: css`
border: 1px solid palevioletred;
background: palevioletred;
color: white;
`,
dark: css`
border: 1px solid #273444;
background: #273444;
color: white;
`,
light: css`
border: 1px solid #eff2f7;
background: #f9fafc;
color: #273444;
`
};
const StyledButton = styled(Button)`
${({ size = "small" }) => sizes[size]};
${({ themes = "primary" }) => ButtonThemes[themes]};
outline: none;
border-radius: 5px;
cursor: pointer;
`;
export default StyledButton;
This is my parent component. Where I used settimeout and import Button component.
const handleClick = () => {
setTimeout(() => {
console.log("check it out") //It does display when the button click
}, 5000);
}
//This is the Button
<Button size="medium" themes="primary" onClick={handleClick}>click</Button>

You should create a loading state by using useState hook if your parent component is function component, if using class component you can define a state in constructor like this this.state = { loading: false } in your parent component, and set it to true in handleClick and pass the state as prop to your Button component:
// In case function component
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
setTimeout(() => {
setLoading(false); // When finish, set it to false
console.log("check it out") //It does display when the button click
}, 5000);
}
//This is the Button
<Button
size="medium"
themes="primary"
onClick={handleClick}
loading={loading}
>
click
</Button>

Related

How to add a state and a list of links displayed in react js and storybook?

I need to have the accordion component that will change over a state when user clicks on the icon.
When user clicks on the icon, the icon and color of the title should get changed and list of links should display.
So far I managed to add onClick option to the component but what will be the best approach to achieve the result of a list displaying over a click?
How to make the icon change over click and display a list with storybook?
Could you please kindly give some advise?
Any help would be appreciated.
I already added two options for the icon, so I think I can add somehow a state to it and make it display on storybook, but not really sure how :(
Here is the component:
import { string, oneOf, func, bool } from "prop-types"
import Icon, { icons } from "design-system/components/icon"
import * as Styled from "./Button.styled"
const Button = ({
href,
text,
iconStart,
iconEnd,
variant,
color,
size,
active,
onClick,
}) => (
<Styled.Component
as={href ? `a` : `button`}
variant={variant}
color={color}
size={size}
href={href}
onClick={onClick}
>
{iconStart && (
<Styled.Icon>
<Icon name={iconStart} size={size} />
</Styled.Icon>
)}
<Styled.Text variant={variant} color={color} active={active}>
{text}
</Styled.Text>
{iconEnd && (
<Styled.Icon>
<Icon name={iconEnd} size={size} />
</Styled.Icon>
)}
</Styled.Component>
)
Button.propTypes = {
text: string.isRequired,
href: string,
iconStart: oneOf(Object.keys(icons)),
iconEnd: oneOf(Object.keys(icons)),
variant: oneOf(["fill", "border", "text", "textLine"]),
color: oneOf(["primary", "black", "white"]),
size: oneOf(["small", "medium", "large"]),
active: bool,
onClick: func,
}
Button.defaultProps = {
href: null,
iconStart: null,
iconEnd: null,
variant: "fill",
color: "primary",
size: "medium",
active: null,
onClick: null,
}
export default Button
Here are the styles:
import styled from "#emotion/styled"
import { css } from "#emotion/react"
import theme from "design-system/theme"
const sizes = {
small: {
typography: theme.typography.desktop.bodySmall,
padding: "8px 32px",
},
medium: {
typography: theme.typography.desktop.h5,
padding: "10px 40px",
},
large: {
typography: theme.typography.desktop.h4,
padding: "12px 48px",
},
}
const colors = {
primary: {
mainColor: theme.colors.primary[500],
filledText: theme.colors.neutrals[100],
},
black: {
mainColor: theme.colors.grey[600],
filledText: theme.colors.neutrals[100],
},
white: {
mainColor: theme.colors.neutrals[100],
filledText: theme.colors.primary[300],
},
}
export const Component = styled.a`
text-align: center;
border-radius: 10px;
padding: 19px;
position: relative;
color: ${({ theme }) => theme.colors.grey[600]};
background-color: ${({ theme }) => theme.colors.complementary[100]};
`
export const Text = styled.span`
${({ variant }) =>
variant === "textLine" &&
css`
text-decoration: underline;
`}
${({ active, variant, color }) =>
active &&
variant === "text" &&
css`
border-bottom: 2px solid ${colors[color].mainColor};
`}
`
export const Icon = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
`
and storybook:
import { Meta, Canvas, Story, ArgsTable } from "#storybook/addon-docs"
import Button from "design-system/components/button"
import Icon from "design-system/components/icon"
<Meta title="Components/Button" component={Button} />
# Button
<Canvas>
<Story
name="Overview - button icon end"
args={{
text: "O nas",
iconEnd: "arrowDown",
variant: "fill",
size: "medium",
}}
>
{Template.bind()}
</Story>
</Canvas>
<ArgsTable />
export const Template = (args) => <Button {...args} />

react styled-component nested item's props

My problem is about props. I want to use nested components with props in styled-components. For example:
const MenuItem = ({ item }) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const isActive = router?.asPath === item?.path;
return (
<MenuItemWrapper key={item?.slug} onClick={() => setIsOpen(!isOpen)}>
<Link href={item?.path}>
<InnerMenuItem isActive={isActive}>
{item?.prefix && <Prefix>{item?.prefix}</Prefix>}
{item?.label}
{item?.children && <RightArrow isOpen={isOpen} />}
</InnerMenuItem>
</Link>
<Children>
{
item?.children?.map((child) => <MenuItem item={child} />)
}
</Children>
</MenuItemWrapper>
);
};
export default MenuItem;
this is MenuItem component. I use MenuItem component as a recursion component.
in styled-component i tried this but it doesnt work. I want to apply different style in Children > InnerMenuItem but it not working
export const Children = styled.div`
display: flex;
flex-direction: column;
margin-left: 65px;
${MenuItemWrapper} {
font-size: 16px;
padding: 9px 0;
&:not(:first-child) {
border-top:none;
}
}
${InnerMenuItem} {
${({ isActive }) => // HOW CAN I USE THIS PROPS HERE
isActive &&
css`
color: orange
`};
}
`;
from styled components official documentations:
"If the styled target is a simple element (e.g. styled.div), styled-components passes through any known HTML attribute to the DOM. If it is a custom React component (e.g. styled(MyComponent)), styled-components passes through all props."
example :
const Input = styled.input`
color: ${props => props.inputColor || "palevioletred"};
`;
return(
<Input inputColor="rebeccapurple" />
)
another way is by Extending Styles, example :
const Button = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
return(
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);
more about styled-components read here
have you tried
${InnerMenuItem} {
color: ${({isActive})=> isActive ? 'orange' : undefined}
};

How can I reuse other external styled-components

I want to load an external styled component and use extending styles, but it doesn't work. How can I solve this problem?
common/Button.js
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 1rem 2rem;
border: none;
border-radius: 8px;
`;
const Button = ({ children, onClick }) => {
return <StyledButton onClick={onClick}>{children}</StyledButton>;
};
export default Button;
pages/Login.js
import Button from '../Components/common/Button';
import styled from 'styled-components';
const StyledButton = styled(Button)`
// not working
color: red;
`;
const Login = () => {
return (
<div>
<StyledButton>Submit</StyledButton>
</div>
);
}
export default Login;
enter image description here
Component can be styled only if it passes className property to it's child. So you should make your Button component like this:
const Button = ({ children, onClick, className }) => {
return <StyledButton onClick={onClick} className={className}>{children}</StyledButton>;
};

How to override Material UI styles using classes using Emiotion

I am trying to override the default styles in a Mui component using the classes prop and passing the css into it. So far I have been unsuccessful and I'm not sure what I have wrong.
The motivation for not using makeStyles is to keep our files cleaner and Emotion is faster.
I am able to do it successfully using makeStyles like this:
import { css } from '#emotion/react';
import HelpIcon from '#material-ui/icons/Help';
import { Tooltip } from '#material-ui/core';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import { useState } from 'react';
import { cbNeutral } from '../../src/theme/palette';
import { makeStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
const useStyles = makeStyles(() => ({
arrow: {
'&:before': {
border: `1px solid ${cbNeutral[900]}`
},
color: cbNeutral[1000]
},
tooltip: {
backgroundColor: cbNeutral[1000],
border: `1px solid ${cbNeutral[900]}`,
color: cbNeutral[400],
boxShadow: '0px 8px 16px 0px rgba(14, 13, 38, 0.12)',
maxWidth: '385px',
fontSize: '1.125rem',
padding: '18px 25px',
fontWeight: 400
},
root: {
fill: cbNeutral[700],
'&:hover': {
fill: cbNeutral[400],
cursor: 'pointer'
}
}
}));
const ToolTip = ({ text }) => {
const styles = useStyles();
const [open, setOpen] = useState(false);
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<Tooltip
arrow
title={text}
placement="right-start"
open={open}
classes={{
arrow: styles.arrow,
tooltip: styles.tooltip
}}
>
<HelpIcon onClick={() => setOpen(true)} classes={{ root: styles.root }} />
</Tooltip>
</ClickAwayListener>
);
};
export default ToolTip;
ToolTip.propTypes = {
text: PropTypes.string.isRequired
};
I am trying to do it with Emotion like this:
import { css } from '#emotion/react';
import HelpIcon from '#material-ui/icons/Help';
import { Tooltip } from '#material-ui/core';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import { useState } from 'react';
import { cbNeutral } from '../../src/theme/palette';
import { makeStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
const toolTipCss = css`
.arrow {
color: ${cbNeutral[1000]};
&:before {
border: 1px solid ${cbNeutral[900]};
}
}
.tooltip {
background-color: ${cbNeutral[1000]};
border: 1px solid ${cbNeutral[900]};
color: cbNeutral[400];
box-shadow: 0px 8px 16px 0px rgba(14, 13, 38, 0.12);
max-width: 385px;
font-size: 1.125rem;
padding: 18px 25px;
font-weight: 400;
}
.help-icon {
fill: ${cbNeutral[700]};
&:hover {
fill: ${cbNeutral[400]};
cursor: pointer;
}
}
`;
const ToolTip = ({ text }) => {
const [open, setOpen] = useState(false);
return (
<div css={toolTipCss}>
<ClickAwayListener onClickAway={() => setOpen(false)}>
<Tooltip
arrow
title={text}
placement="right-start"
open={open}
classes={{
arrow: 'arrow',
tooltip: 'tooltip'
}}
>
<HelpIcon onClick={() => setOpen(true)} className="help-icon" />
</Tooltip>
</ClickAwayListener>
</div>
);
};
export default ToolTip;
ToolTip.propTypes = {
text: PropTypes.string.isRequired
};
In 👆 that version where I use className="help-icon", it works as expected, but classes={{arrow: 'arrow', tooltip: 'tooltip'}} does not change the default MUI styles.
I am using these versions of MUI and Emotion
"#emotion/react": "^11.4.0",
"#material-ui/core": "^4.11.4",
I know we have it working like this in another app using older versions of MUI and emotion. I don't know if something has changed in the mean time or of I just have something wrong with my syntax. Any help would be appreciated.

Approach to creating variants with styled components

What is the best way to create variants using styled components? Heres what i am currently doing.
const ButtonStyle = styled.button`
padding:8px 20px;
border:none;
outline:none;
font-weight:${props => props.theme.font.headerFontWeight};
font-size:${props => props.theme.font.headerFontSize};
display:block;
&:hover{
cursor:pointer;
}
${({ variant }) =>
variant == 'header' && css`
background-color:${props => props.theme.colors.lightblue};
color:${({ theme }) => theme.colors.white};
&:active{
background-color:${props => props.theme.colors.blue}
}
`
}
${({ variant }) =>
variant == 'white' && css`
background-color:white;
color:${({ theme }) => theme.colors.lightblue};
&:active{
color:${props => props.theme.colors.blue}
}
`
}
`;
I cannot tell if this is the standard way of doing things.
I have also been using other components as bases to create other components from while changing a few things
eg
const InnerDiv = styled(otherComponent)`
position: unset;
background-color: red;
overflow-x: hidden;
display: flex;
`;
Which is the better approach? Are there any better alternatives?
Inspired by previous solutions, I want to share what I came up with:
import styled, { css, DefaultTheme } from 'styled-components';
const variantStyles = (theme: DefaultTheme, variant = 'primary') =>
({
primary: css`
color: ${theme.colors.light};
background: ${theme.colors.primary};
border: 1px solid ${theme.colors.primary};
`,
}[variant]);
const Button = styled.button<{ variant: string }>`
padding: 1rem;
font-size: 0.875rem;
transition: all 0.3s;
cursor: pointer;
${({ theme, variant }) => variantStyles(theme, variant)}
&:active {
transform: translateY(1.5px);
}
`;
export default Button;
For now it contains only primary and its the default one, by you can add more variants by adding new object to variantStyles object
Then you can use it by passing the variant as a prop or keep the default by not passing any variant.
import { Button } from './HeroSection.styles';
<Button variant="primary">Start Learning</Button>
This is just my opinion:
I don't think we can do anything very different from what you did.
A different way that I thought, would be to create an options object to map the possibilities of the variant, like this:
const variantOptions = {
header: {
backgroundColor: theme.colors.lightblue,
color: theme.colors.white,
active: theme.colors.blue,
},
white: {
backgroundColor: "white",
color: theme.colors.lightblue,
active: theme.colors.blue,
},
};
And use it in your style component like this:
const ButtonStyle = styled.button`
padding: 8px 20px;
border: none;
outline: none;
font-weight: ${(props) => props.theme.font.headerFontWeight};
font-size: ${(props) => props.theme.font.headerFontSize};
display: block;
&:hover {
cursor: pointer;
}
${({ variant }) =>
variant &&
variantOptions[variant] &&
css`
background-color: ${variantOptions[variant].backgroundColor};
color: ${variantOptions[variant].color};
&:active {
color: ${variantOptions[variant].active};
}
`}
`;
And all of this buttons will work:
<ButtonStyle variant="*wrong*">Button</ButtonStyle>
<ButtonStyle variant="header">Button</ButtonStyle>
<ButtonStyle variant="white">Button</ButtonStyle>
<ButtonStyle>Button</ButtonStyle>
When dealing with Styled Component variants here is what I like to do to keep things organised and scalable.
If the variants are stored within the same file I am using the inheritance properties:
const DefaultButton = styled.button`
color: ${(props) => props.theme.primary};
`;
const ButtonFlashy = styled(DefaultButton)`
color: fuchsia;
`;
const ButtonDisabled = styled(DefaultButton)`
color: ${(props) => props.theme.grey};
`;
If if we are talking about a reusable components I would use this technique:
import styled from 'styled-components';
// Note that having a default class is important
const StyledCTA = ({ className = 'default', children }) => {
return <Wrapper className={className}>{children}</Wrapper>;
};
/*
* Default Button styles
*/
const Wrapper = styled.button`
color: #000;
`;
/*
* Custom Button Variant 1
*/
export const StyledCTAFushia = styled(StyledCTA)`
&& {
color: fuchsia;
}
`;
/*
* Custom Button Variant 2
*/
export const StyledCTADisabled = styled(StyledCTA)`
&& {
color: ${(props) => props.theme.colors.grey.light};
}
`;
export default StyledCTA;
Usage:
import StyledCTA, { StyledCTADisabled, StyledCTAFushia } from 'components/StyledCTA';
const Page = () => {
return (
<>
<StyledCTA>Default CTA</StyledCTA>
<StyledCTADisabled>Disable CTA</StyledCTADisabled>
<StyledCTAFushia>Fuchsia CTA</StyledCTAFushia>
</>
)
};
Read more about this in the blog posts I created on the subject here and there.
There are many ways to do this. one simple way is to use the package called Styled-components-modifiers. documentation is simple and straightforward.
https://www.npmjs.com/package/styled-components-modifiers
Simple usage example:
import { applyStyleModifiers } from 'styled-components-modifiers';
export const TEXT_MODIFIERS = {
success: () => `
color: #118D4E;
`,
warning: () => `
color: #DBC72A;
`,
error: () => `
color: #DB2A30;
`,
};
export const Heading = styled.h2`
color: #28293d;
font-weight: 600;
${applyStyleModifiers(TEXT_MODIFIERS)};
`;
In the Component - import Heading and use modifier prop to select the variants.
<Heading modifiers='success'>
Hello Buddy!!
</Heading>
Styled components are usually used with Styled system that supports variants and other nice features that enhance Styled components. In the example below Button prop variant automatically is mapped to keys of variants object:
const buttonVariant = ({ theme }) =>
variant({
variants: {
header: {
backgroundColor: theme.colors.lightblue,
color: theme.colors.white,
active: theme.colors.blue,
},
white: {
backgroundColor: 'white',
color: theme.colors.lightblue,
active: theme.colors.blue,
},
},
})
const Button = styled.button`
${(props) => buttonVariant(props)}
`
Styled System Variants: https://styled-system.com/variants
Use the variant API to apply styles to a component based on a single prop. This can be a handy way to support slight stylistic variations in button or typography components.
Import the variant function and pass variant style objects in your component definition. When defining variants inline, you can use Styled System like syntax to pick up values from your theme.
// example Button with variants
import styled from 'styled-components'
import { variant } from 'styled-system'
const Button = styled('button')(
{
appearance: 'none',
fontFamily: 'inherit',
},
variant({
variants: {
primary: {
color: 'white',
bg: 'primary',
},
secondary: {
color: 'white',
bg: 'secondary',
},
}
})
)

Resources