Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.attrs equivalent #821

Closed
jamiewinder opened this issue Aug 23, 2018 · 32 comments
Closed

.attrs equivalent #821

jamiewinder opened this issue Aug 23, 2018 · 32 comments

Comments

@jamiewinder
Copy link

jamiewinder commented Aug 23, 2018

This was mentioned in #109 just over a year ago but with very little discussion around it so I hope you don't mind me raising it again.

I've been toying with the idea of switching a very large project over from styled-components to emotion for a while now. Today I bit the bullet and tried doing a mass conversion of thousands of components.

I was very pleased and surprised with how straightforward this was to do thanks to your similar APIs. However, the one feature I'm sorely missing is the ability to define default props; .attrs.

Again, in #109 this was raised but it looks like the suggested approach is to use recompose and withProps. Personally I think this would be better as part of the library.

Much of my conversion was a simple find and replace job, but converting .attrs required a comparatively disproportionate amount of effort. Having to depend on another library for the purpose of importing a single function is fine I suppose, but the syntax is also a bit jarring. The nice thing about the styled API is how readable it can be... however when a styled component with some logical defaults comes along it takes a bit more mental parsing than seems necessary.

Compare

const Input = styled.input`
    border: thin solid red;
`;

const Password = styled(Input).attrs({ type: 'password' })`
    color: blue;
`;

or

const Input = styled.input`
    border: thin solid red;
`;

const Password = styled(Input, { type: 'password' })`
    color: blue;
`;

with

const Input = styled.input`
    border: thin solid red;
`;

const Password = withProps({ type: 'password' })(styled(Input)`
    color: blue;
`);

I know much of this is personal preference, but the withProps variation looks so unlike a 'normal' styled component yet the act of specifying some sensible default props isn't really that exotic, is it?

Anyway, just thought I'd put this out there. I know there is personal preference and library 'purity' as factors too. It just seems to me this would be a tiny addition but one which may help fellow wannabe styled components users like me take the leap.

Thanks.

@emmatown
Copy link
Member

We've discussed this before and we've come to the conclusion that the best way to do this is recommend people use the css prop and attach props just like you would any other react component.(the reasons are described in the linked comment)

@jamiewinder
Copy link
Author

Thanks. That's the first thing I tried though there is a gotcha in that the component becomes stateless so can't have refs. This is an issue for libraries which use refs to attach behaviour to elements (react-dnd, react-measure, etc.)

I imagine you could use forwardRef these days but it then starts looking a lot like the withProps code above

@Andarist
Copy link
Member

@jamiewinder what's the gotcha with ref regarding defaultProps (which .attrs are, right?)? Seems like separate subjects but maybe I have misunderstood smth.

@jamiewinder
Copy link
Author

jamiewinder commented Aug 24, 2018

Stateless components can't have refs, so while a standard styled component can be used with things like react-dnd and react-measure without too much trouble (just use innerRef rather than ref), you can't do the same with a stateless wrapper that provides some default props (like the example @mitchellhamilton gave in his reply).

You can if you use forwardRef or just by using withProps (since that creates a class-based / stateful wrapper) but then the code starts becoming (imo) unnecessarily messy like the code above, all for the sake of supplying some logical defaults props that go hand-in-hand with styling when it comes to creating semantic components.

For example, if I have a styled <Flex> component which takes a direction prop, then I might logically want to define a <Column> component which extends Flex, defaults the direction to 'column', and maybe adds some styles of its own.

If I use the approach suggested above, then I suddenly can't use react-measure to measure my Column component. Not without using forwardRef to wrap the component but that's just more noise in what ought to be as readable as a stylesheet.

In styled-components, I can simply:

const Column = styled(Flex).attrs({ direction: 'column' })`
    color: red;
`;

Emotion is already effectively tying props to styles and when components extend other components and may want to define default props (ergo default styles) you'll want to make this as straightforward as possible without the visual noise withProps adds and the pitfalls of a stateless component wrapper (i.r.t. refs as described above).

It just seems to be a logical extension of the emotion's "style as a function of state" tagline - default props = default styles when you're extending one styled component from another.

@Andarist
Copy link
Member

Ok, so I'm not sure if the .withProps is the official recommendation. Personally I would recommend doing this:

const Column = styled(Flex)`
    color: red;
`;
Column.defaultProps = { direction: 'column' }

@jamiewinder
Copy link
Author

Thanks, I think that's my preferred method so far.

@lundgren2
Copy link

Did this ever go anywhere? Seems really useful!

I think the method mentioned by @Andarist just before are such as good as the .attr() way? and also more React familiar.

@nelsieborja
Copy link

I'll go with @Andarist's solution. This is the best solution as it makes sense to add type as a default prop. Thanks man, I almost forgot this specific advantage of using defaultProps

@Andarist
Copy link
Member

Andarist commented Oct 23, 2019

The preferred way of doing this is just using @emotion/core APIs and because with them everything is done inside of your component's render there is no need to introduce any custom API to emotion itself, because you can just use JavaScript to handle your stuff. If you like styled API you can also easily build your own flavor of it based on the @emotion/core APIs (taking @emotion/styled-base implementation as a base for it - as it is actually just a wrapper around @emotion/core).

EDIT:// Oh, and I would not recommend using .defaultProps nowadays as React team is planning to remove support for this entirely in the future - they are taking "just JavaScript" philosophy as the argument for that.

@Nantris
Copy link
Contributor

Nantris commented Oct 23, 2019

I'm not clear on what the recommended alternative is. Can you clarify @Andarist, or is there some link to a relevant doc? This issue was the best info I found on achieving this end. Turns out it was a bad week to implement defaultProps for styled components.

@Andarist
Copy link
Member

You can just not use styled API at all, but if you really want to you can make hoops like this - https://codesandbox.io/s/emotion-uj5z9

import styled from "@emotion/styled";

const customStyled = (tag, options) => {
  const styledFactory = styled(tag, options);
  return function() {
    const Styled = styledFactory.apply(null, arguments);
    Styled.attrs = extraProps => {
      const WithAttrs = props => {
        return <Styled {...extraProps} {...props} />;
      };
      for (let key in Styled) {
        WithAttrs[key] = Styled[key];
      }
      Object.defineProperty(WithAttrs, "toString", { value: Styled.toString });
      return WithAttrs;
    };
    return Styled;
  };
};

@FezVrasta
Copy link
Member

FezVrasta commented Oct 24, 2019

I'm trying to understand... the proposed solution is to implement the feature ourselves?

How does this plays with the Babel Macro version? Can I just import @emotion/styled/macro and it will work?

@Andarist
Copy link
Member

I'm trying to understand... the proposed solution is to implement the feature ourselves?

We believe that this particular feature is not worth including in the core. We need to think about APIs we provide and can't just satisfy each possible feature request. Also please see this comment - #617 (comment) .

How does this plays with the Babel Macro version? Can I just import @emotion/styled/macro and it will work?

Unfortunately not.

@Andarist
Copy link
Member

Also, regular React-way of composition works for this just OK.

const Input = styled.input``
const TextInput = props => <Input {...props} type="text" />

Even if this seems like a boilerplate this is just a standard way of composing React component, no special APIs, just React and JavaScript.

@FezVrasta
Copy link
Member

I see, to be honest I feel like this is a quite common use case. I will most likely use .defaultProps until React will support it, since it seems the cleaner approach.

But if defaultProps is ever going to be removed I feel like some easy to use API provided by Emotion directly will be much needed.

@Nantris
Copy link
Contributor

Nantris commented Oct 24, 2019

The React composition method works, but it definitely seems like a big step backwards from the simplicity of the defaultProps. I understand if the emotion team thinks it's not worth the effort required to make/support the feature, but the composition method is more verbose and repetitive.

@Andarist
Copy link
Member

There are multiple ways to achieve this - without needing to put this into the core. There is always a recompose~ approach (withProps) which you can also implement yourself with 1-liner:

const withAttrs = (Component, attrs) => props => <Component {...attrs} {...props}/>

@Nantris
Copy link
Contributor

Nantris commented Oct 25, 2019

Thanks @Andarist! While still not as clean as defaultProps, the recompose method seems better than any of the other approaches.

@S1lentium
Copy link

S1lentium commented Oct 26, 2019

recompose withProps does not send props (attributes) from the HTML element (StyledComponent):

const Range = withProps({ type: 'range' })(styled(Input)`
    color: blue;
`);
<Range min="0" max="1" step="0.01" onChange={handleVolume} value={state.volume} />

Type 'string' is not assignable to type 'never'

@topaxi
Copy link

topaxi commented Nov 11, 2019

One thing that isn't noted here is that attrs() allows to map props.

const MyComponent = styled.div.attrs(props => {
  return {
    cssWidth: props.width
  }
})`
  width: ${props => props.cssWidth};
`

MyComponent.propTypes = {
  width: PropTypes.number
}

Usage without providing a function doesn't necessarily give any value over the defaultProps approach.

Having support for the above use case would massively help in migrating from styled-components to emotion.

@verekia
Copy link

verekia commented Feb 4, 2020

Is there any way to do what @Andarist suggested with the object notation?

const Column = styled(Flex, { color: 'red' });

This doesn't seem to work.

@Andarist
Copy link
Member

Andarist commented Feb 4, 2020

@verekia I'm not sure what you are requesting here, but the used syntax is wrong - take a look like how this kind of thing should be written: https://codesandbox.io/s/xenodochial-http-ydqkx

@verekia
Copy link

verekia commented Feb 6, 2020

Thank you very much! It works well :)

Maybe it would be useful to add this example to the Styled documentation. There is no example with this syntax: styled(foo)(styles) if I'm not mistaken.

@Andarist
Copy link
Member

Andarist commented Feb 6, 2020

PRs are welcome :)

@lior-chervinsky
Copy link

lior-chervinsky commented Jun 11, 2021

Any chance this issue will be re-examined?
I find the suggested solutions not satisfying.
My use case is trying to create a styled button with a default type="button" attribute.
The most acceptable solution suggested is to wrap the component, creating a basic styledButton and then the Button.

But it is too verbose and creates a useless wrapper object.
This is repetitive and honestly a deal breaker from my point of view.. (perhaps i'm missing something?)
If the styled "CSS" component requires a wrapper to support default attributes - then i might prefer using StyledComponents since its API supports a simpler and cleaner code. (at least in this case)

const StyledButton = styled('button')<{ color? : string, border?}>`
  color: ${props => props.color ? props.color : '#004f6b'};
  ... (more css etc.)
`

const Button = (props => <StyledButton type='button' {...props}/>

This also requires also importing React just for composing the component with attributes..
My Button is on a separate file, which means that without the type attribute i would only import Emotion and not React

@Andarist @jamiewinder @mitchellhamilton WDYT?

@joemaffei
Copy link

My use case is trying to create a styled button with a default type="button" attribute.

@lior-chervinsky Would this work for you?

const StyledButton = styled.button``;
StyledButton.defaultProps = { type: 'button' };

@remy90
Copy link

remy90 commented Dec 13, 2021

@joemaffei What about when you want if you want to pick an attribute value based on a condition or a prop?
i.e.

const StyledButton = styled.button``;
StyledButton.defaultProps = p => { type: p.type };

I know that syntax is junk, but does it get my question across?

@Andarist
Copy link
Member

Recently I got a little bit more inclined to revisit this. If somebody wants to implement this with proper TS support - let me know and let's discuss this in a new issue (don't jump straight to coding!)

@joemaffei
Copy link

@joemaffei What about when you want if you want to pick an attribute value based on a condition or a prop?
i.e.

const StyledButton = styled.button``;
StyledButton.defaultProps = p => { type: p.type };

I know that syntax is junk, but does it get my question across?

@remy90 I suggested defaultProps because it's built into React, and it's a valid solution to the problem posed by @lior-chervinsky: a button with a default type of "button". I understand that the solution is not as robust as attrs, but it's an adequate alternative in cases where the defaults are static and not derived from props.

@csvan
Copy link

csvan commented Apr 19, 2022

One thing that isn't noted here is that attrs() allows to map props.

const MyComponent = styled.div.attrs(props => {
  return {
    cssWidth: props.width
  }
})`
  width: ${props => props.cssWidth};
`

MyComponent.propTypes = {
  width: PropTypes.number
}

Usage without providing a function doesn't necessarily give any value over the defaultProps approach.

Having support for the above use case would massively help in migrating from styled-components to emotion.

You could write a more complex wrapper which accomplishes this, e.g.

import { useTheme } from '@emotion/styled';

export const withAttrs =
  (Component, fn) =>
  (props) => {
    const theme = useTheme();
    const attrs = fn({ theme, props });

    return <Component {...props} {...attrs} />;
  };

and then:

const StyledCard = withAttrs(styled(Card)`
  display: flex;
  flex-direction: column;
`, ({props, theme}) => ({
  color: props.active ? theme.linkColor.text : 'transparent',
}))

@emilianomon
Copy link

emilianomon commented Nov 28, 2022

I've been trying to solve this issue with a HOC and Typescript for sometime now, but found it to be the "wrong" approach. The main reason is that I have not found a way to infer from the component props' attributes that must become optional after setting some statics.

Since I believe this must be an issue for other folks as well, I though it would be nice to share my workaround and also to ask if anyone has solved the issue presented in the last paragraph in a more generic and sophisticated way.

// File: utils/types.ts
export type MakeOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
// File: input.styles.tsx
// ...

const ClearIcon = styled(Icon)`
  align-self: center;
  height: 30px;
  width: 30px;
`;

const clearIconStyle = css`
  stroke: red;
`;

export const Styles = {
  ClearIcon: (props: MakeOptional<IconTypes.Props, 'name'>) => (
    <ClearIcon {...props} 
      name={IconTypes.Name.Close}
      iconStyle={clearIconStyle} 
    />
  ),
  // ...
};
// File: input.tsx
// ...

export const Input: FC<InputTypes.Props> = props => (
  <Styles.Wrapper style={props.style}>
    // ...
    {props.clearable && (
      <Styles.ClearIcon />
    )}
    // ...
  </Styles.Wrapper>
);

If anyone knows a better way to address the issue, please respond to this comment.

@strblr
Copy link

strblr commented Jan 15, 2023

@emilianomon I was facing the same issue, here is what I came up with:

type Defaultize<P extends {}, Q extends Partial<P>> = Omit<P, keyof Q> &
  Partial<Pick<P, keyof Q & keyof P>>;

export function attrs<P extends {}, Q extends Partial<P>>(
  propsFactory: Q | ((props: Defaultize<P, Q>) => Q),
  Component: ComponentType<P>
) {
  return forwardRef<ComponentRef<ComponentType<P>>, Defaultize<P, Q>>(
    (props, ref) => {
      return (
        <Component
          {...(props as P)}
          {...(isFunction(propsFactory)
            ? propsFactory(props)
            : propsFactory)}
          ref={ref}
        />
      );
    }
  );
}

A bit hacky but it seems to work:

type Props = {
  foo: number;
  bar: boolean;
  baz?: string;
};

const A = styled((_: Props) => <div />)`
  color: red;
`;

const B = attrs({}, A);
const C = attrs({ foo: 45 }, A);
const D = attrs({ bar: true }, C);
const E = attrs({ foo: 45, bar: true }, A);

const a = <A />; // Error, "foo" and "bar" missing
const b = <B />; // Error, "foo" and "bar" missing
const c = <C />; // Error, "bar" missing
const d = <D />; // Ok
const e = <E />; // Ok

Something like this might also work (not tested on my side).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests