Styled-components v4 has introduced this, which seems like a useful thing to me.
It is neat to specify the underlaying component dynamically instead of creating all variants upfront. The feature was also introduced previously in completely separate project - https://github.com/reakit/reakit (built in on top of styled-components v3). Not sure if they have consulted/discussed it, but seems like it has proven to be useful as it got implemented in the core API.
I've used it a bunch in userland when creating design systems. It's great for end-users because it's "just another prop" and has a nice interaction with is for variants when building component APIs, which you can <Thing is="primary" as="section"> kinds of things.
Would love to see this "in core" for v10
I can prepare a PR (should be super straightforward) for this if we agree that this is something that we'd like to incorporate into the core.
I am just starting a component library and am testing out emotion in it. I actually implemented the as prop myself on a component and am wondering if there are functional differences compared to a built-in as prop.
Here is the Heading component:
import React, { SFC, ReactNode } from 'react';
import styled, {css} from "react-emotion";
export enum HeadingTags {
h1 = "h1",
h2 = "h2",
h3 = "h3",
h4 = "h4",
h5 = "h5",
h6 = "h6",
}
export interface HeadingProps {
/**
* Element tag to render (h1-h6)
* @default h1
**/
tag?: HeadingTags;
/**
* Heading text
* @default null
**/
children?: ReactNode;
}
const headingCss = css({
color: 'purple',
fontSize: '24px',
});
export const Heading: SFC<HeadingProps> = ({ children = null, tag = HeadingTags.h1 }) => {
const MyHeading = styled(tag)(headingCss);
return <MyHeading>{children}</MyHeading>;
};
For styled-components, if you use our as prop on a wrapped styled component we will make sure to flatten and retain the styles internally. Not sure if that'll be relevant to the emotion implementation too, but here's an example:
import styled from "styled-components"
const RedColor = styled.div`
color: red;
`
const BlueBackgroundRedColor = styled(RedColor)`
background: blue;
`
<BlueBackgroundRedColor as="span">Hello!</BlueBackgroundRedColor>
// Even though we switch to rendering a `span` from rendering
// <RedColor />, this will still have a red color on top of
// the blue background!!
You cannot make that work in user-land, that will break with your as prop.
It actually can be implemented user land with smth like:
export const Heading = styled(({
as: T = 'h1',
...props
}) => <T {...props}/>)({
color: 'purple',
fontSize: '24px',
})
Which is better than a @alexkrautmann version as it doesn't recreate new component types in render.
As to the component flattening - we do the same in emotion, but for as it doesn't quite matter (nor it does in styled-components case).
The only open question for us is how should we handle shouldForwardProp in combination with as.
The only open question for us is how should we handle shouldForwardProp in combination with as.
I'm wondering this as well.
@alexkrautmann creating a styled component on every render is ill-advised. It would be much simpler to use css to create a className and return the element.
e.g.
export const Heading: SFC<HeadingProps> = ({ children = null, tag = HeadingTags.h1 }) => {
const myClassName = css(headingCss);
return React.createElement(tag, { className: myClassName, children })
};
I鈥檓 good with adding this.
For shouldForwardProp, I think it should probably be use shouldForwardProp if the option is defined otherwise use the type of the as prop and if as isn鈥檛 used, use the type passed to styled.
Cool, going to prepare a PR for this in following days
how does this work when the component your styling also has a as prop?
This would have to be a "reserved" prop - so it wouldn't be forwarded to the underlaying component.
hmm that doesn't jive super well with ui frameworks, namely semantic-ui and react-bootstrap, both use as to control the underlying component. There needs to be a way to forward through props or specify both, e.g. as and childAs, or more generally as and something like innerProps sort like how Matertial-ui does it
Could u paste in a sample snippet of how at the moment is emotion used together with i.e. semantic-ui and as prop?
sure, rendering a semantic Button component as a 'a' instead of a 'button'
import { Button } from 'semantic-ui-react'
const FooterButton = styled(Button)`
margin-top: 30px
`
<FooterButton as="a" href="/link/styled/as/Button"/>
In this case, why wouldn't you make the "as" target of Button the styled/emotion component?
import { Button } from 'semantic-ui-react'
const FooterButton = styled.a`
margin-top: 30px
`
<Button as={FooterButton} href="/link/styled/as/Button"/>
you could do that in this simple example, but i don't think that's a natural way of writing it.
That also doesn't work if you need to style based on the Button props vs the DOM element ones e.g.
styled(Button)`
margin-top: ${p => p.basic ? 20 : 30}px
`
@jquense Button doesn't pass through props in an "as" scenario?
import { Button } from 'semantic-ui-react'
const FooterButton = styled.a`
margin-top: ${p => p.basic ? 20 : 30}px;
`
<Button basic as={FooterButton} href="/link/styled/as/Button"/>
They really should if not, since they have no idea what they're rendering underneath.
They really should if not
That wouldn't make sense or work unfortunately, basic is not a DOM attribute, its a prop consumed by and for the Button, what the rendered element gets is a style or class etc. This is for controlling the underlying rendered element not controlling the Button component
We forward quite every prop if we render a non-dom element, what props are forwarded can be also customized with shouldForwardProp option. The snippet given by @probablyup should "just work" for you though, could you specify what is the exact problem with it?
EDIT:// Oh, you could have meant that semantic-ui-react is not forwarding props appropriately, is that the problem?
I don't understand how it would work. emotion both uses the as prop and also passes it through? The problem is that FooterButton should render Styled(Button) -> <Button as='a'> -> a, but if it uses the as the prop it will render Styled(a) -> a which is not correct.
The changed snippet does not work for the original case Styled(a) is not passed basic by Button, so there is no way for FooterButton to respond to Button props for styling if you pass Styled('a) to <Button as={}>
I see what do u mean - with emotion you could do this:
import { Button } from 'semantic-ui-react'
// you can customize `shouldForwardProp` here
const FooterButton = styled(Button, { shouldForwardProp: () => true })`
margin-top: ${p => p.basic ? 20 : 30}px
`
<FooterButton as="a" href="/link/styled/as/Button"/>
This is still somewhat not-ideal, because shouldForwardProp is static & as is dynamic configuration, so a single shouldForwardProp per component would have to handle all your possible as overloads.
Right but Button should definitely be passing props through to the child if the child isn't a simple element. A string vs function check is sufficient to make this determination.
I disagree that that would be the correct behavior for Button. components shouldn't arbitrarily pass all their props through to their children.
I'm also inclining to disagree on that. This rule IMHO applies to css-in-js libraries, but semantic-ui isnt one - and from their perspective it's totally valid to use some props & skip forwarding them, regardless of the rendered type.
EDIT:// my last given snippet wouldnt work, as prop would get forwarded, but to the wrong component 馃槶
I would love if we could come up with a more standard pattern for handling this sort of prop conflict, it affects SC as well as the ui libraries that use it, not just css-in-js ones. The below does technically avoid the problem but not ideal for verbosity and only really works for the css-in-js pure wrapper case.
const StyledButton = styled(Button)`
margin-top: ${p => p.basic ? 20 : 30}px
`
const LinkButton = props => <Button as="a" {...props} />
<StyledButton as={LinkButton} basic />
Maybe we could make it so as is only used by emotion/SC if the the element type in the styled call is a string. I can鈥檛 think of a use case where you would want to use as and have the default element type not be a string so I think it addresses the original use case and solves the conflict problem.
Maybe we could make it so as is only used by emotion/SC if the the element type in the styled call is a string.
Hmm. "as" on a styled(CustomComponent) is essentially the equivalent of withComponent. So perhaps there is still a use case for withComponent and we won't deprecate and remove it.
The only thing is I don't like forcing the composer to have to know details of what the StyledComponent is wrapping. It makes us end up in the innerRef backflip again of having to know too much ahead of time to be able to use the API effectively.
I'm firmly of the opinion that components which wrap other components should forward all props they don't know about or are not directly responsible for -- not going to change my mind on that.
I'm firmly of the opinion that components which wrap other components
I think thats fine, the examples here tho are not wrapping components, they are the component, its a fundamentally different thing that what emotion is doing, it just looks the same b/c of the similarities.
I think thats fine, the examples here tho are not wrapping components, they are the component, its a fundamentally different thing that what emotion is doing, it just looks the same b/c of the similarities.
Yeah, exactly.
@probablyup
Taking your last snippet of:
import { Button } from 'semantic-ui-react'
const FooterButton = styled.a`
margin-top: ${p => p.basic ? 20 : 30}px;
`
<Button basic as={FooterButton} href="/link/styled/as/Button"/>
Button here is responsible for handling basic prop, thus it makes sense for it not to forward basic to the wrapped element - imho no matter what kind of component it is wrapping. And this effectively disallows reusing such prop for styling purposes
The only thing is I don't like forcing the composer to have to know details of what the StyledComponent is wrapping. It makes us end up in the innerRef backflip again of having to know too much ahead of time to be able to use the API effectively.
Does that matter if there's no use for using as with a custom component in the styled call? I could be wrong but I can't see a use case for it.
Potential use case - svgs. I can easily imagine creating a styled with custom svg component and wanting to use as to change the underlaying component.
Potential use case - svgs. I can easily imagine creating a styled with custom svg component and wanting to use as to change the underlaying component.
Could you give a code example of this?
Smth like
const Arrow = styled(ArrowSvg)``
<Arrow as={MaterialUiArrow}>
Basically recreating more general .withComponent use case (than <a/> & <button/>-like cases) but in dynamic manner of as prop.
Wouldn't that mean, that I have to throw in React/JSX into my styled components if I want to use this feature? I sometimes write stuff like this, that wouldn't work anymore without using JSX:
export const Button = styled.button``
export const ButtonLink = Button.withComponent('a')
It depends on if this would make us remove withComponent, that is not set in stone.
You could also write a simple helper such as
const withComponent = (Comp, replacement) => props => <Comp as={replacement} {...props} />
and use it like this:
export const Button = styled.button``
export const ButtonLink = withComponent(Button, 'a')
REPOST:
Posted this on the current PR and referencing @Andarist current draft of the feature. Will post it here for the discussion.
referenced Commit
@Andarist looking at the other discussion about breaking some of the semantic-ui / react-bootstrap components, and looking at your current draft, how about having the option to disable the as-prop feature.
For example having a option api 'enableAs' (prob a better name for it) that is default to true.
And pass that to your getShouldForwardProp, and also use it to determine finalTag.
```javascript
const testOmitPropsOnComponent = (asEnabled:boolean)=>(key: string) =>
key !== "theme" && key !== "innerRef" && asEnabled? key !== "as":true ;
export const testAlwaysTrue = () => true;
export const getShouldForwardProp = (tag, asEnabled) =>
typeof tag === "string" &&
// 96 is one less than the char code
// for "a" so this is checking that
// it's a lowercase character
tag.charCodeAt(0) > 96
? testOmitPropsOnStringTag
: testOmitPropsOnComponent(asEnabled);
````
then in finalTag..
```javascript
const finalTag = options.enableAs? props.as || baseTag : baseTag ;
````
so @jquense snippet would work by just disabling.. ?
```javascript
import { Button } from 'semantic-ui-react'
const FooterButton = styled(Button,{enableAs:false})
margin-top: 30px
````
For me, im a big fan for the as prop, but i do agree with you in trying to support existing ecosystems. I think adding this additional api, is worth it considering the amount of people that want this feature and the amount of people that also rely on systems like semantic-ui and react-bootstrap.
Config option is definitely one way to go. I clutters the codebase and the API though so I'm still wondering if there is any way to support both worlds "out of the box" - cant figure it out though.
styled-component@4 should come out of beta soon, so I'm wondering how this change will play out for them. As this has to be introduced in emotion@10 (not worth to introduce it to emotion@9 because of compatibility issues) we still have some time to figure this out (and ofc to decide if we want to implement this).
hmm another i can think of to not clutter the api but may add more to codebase is to essentially do what is being done above, but automatically. "enabled" if the underlying tag is a string or a styled component and "disabled" if not. Maybe by adding in another __emotion property to determine when to enable/disable? That way "as" prop always gets passed down and baseTag is always the finalTag if baseTag is a unknown/nonstyled component ?
just read above and realize what i just type was essentially what @mitchellhamilton and others mentioned above. sorry for repeating whats already been discussed.
sorry, one more spitball. How about a special prefix? Like styletron does. They just simply omit any props prefixed with an '$'.
It would be nice if all css-js libs followed same type of norm for these types of issues.
IMO i dont mind the prefix, and do like knowing for all props prefix with a $, it has something to do with a styled component and wont be rendered to dom.
const theme = {colors: { blue: 'DarkBlue', blueText: 'white' }}
<Button as='link' theme ={theme}) color='blue'/>
<Button $as='link' $theme ={theme}) $color='blue'/>
const shouldForwardProp=key=>testOmitPropsOnStringTag(key) && key != "color" && key != "as"}
const getButtonStyles = ({ color, theme }) => ({
backgroundColor: theme.color[color] || "white",
color: theme.color[color + "Text"] || "black"
});
const Button = styled("button", {shouldForwardProp})(getButtonStyles)
vs
const shouldForwardProp=key=>key[0] !== "$"
const getButtonStyles = ({ $color, $theme }) => ({
backgroundColor: $theme.color[$color] || "white",
color: $theme.color[$color + "Text"] || "black"
});
const Button = styled("button", {shouldForwardProp})(getButtonStyles)
sidenote:
I found styletron by looking into uber-web/basui methodology which i have found to be unique and extremely flexible(in terms of altering nested styled-components without selectors) and personally will probably consume some of their coding and organization techniques.
The prefixed prop thing is being discussed for styled-components also, but haven't made a decision yet. It would be for handling styling props though, not "as".
I think the pass through option is a better solution for styled case. My hunch here is you'd probably only ever want one behavior for a component. E.g. you always want to pass through the prop or you dont. It's seems pretty unlikely that you'd style something like a Button and also want to attach those styles to a completely different, non Button component.
In other words I think it's reasonable to say only one thing is ever responsible for handling the as prop and that would be easy enough to know upfront
Not to mention a prefixed prop is a moving the target. Tho maybe this is a good case allowing symbol props in jsx
I think what we could do is respect the shouldForwardProp option for as so if shouldForwardProp('as') returns true, emotion won't use it and just pass it through and if shouldForwardProp('as') returns false, emotion will use it. So the default behavior would be that as is used for string tags and passed through for other components but could still be used for other components if people want to.
Liking it! Bummer is that it cant work out of the box for both cases, but it is as it is. Gonna work on implementing this.
Maybe we could make it so as is only used by emotion/SC if the the element type in the styled call is a string. I can鈥檛 think of a use case where you would want to use as and have the default element type not be a string so I think it addresses the original use case and solves the conflict problem.
A bit late to this, but a common use case we have is this - we have a component library that exports components that are expected to be anchors. We don't want to embed react-router in our component library but expect users of the component library to want to replace the anchor with a React Router Link. @Loque- recently asked about this on the emotion #dev slack channel
Update from the styled-components side: we moved forward with "as" as-is for v4. We have a candidate change lined up for v5 / v4.1 that may solve the forward or not forward part of the conversation.
With React Router we have an associated issue of handling a React Router active NavLink - https://github.com/ReactTraining/react-router/issues/6390
Instead of config options, you could do this when needed:
import { Button } from 'semantic-ui-react'
const FooterButton = styled(({ nativeAs, ...rest }) => <Button as={nativeAs} {...rest} />)`
margin-top: 30px
`
<FooterButton nativeAs="a" href="/link/styled/as/Button"/>
It's not very pretty, but unless you're doing this everywhere, I think it's better than cluttering the API.
Sure, however it's innerRef/forwardRef/hostRef problem all over again. Currently implemented (there is a pending pull request) changes that intend to support this in the core doesn't clutter the API that much, it just adds a special handling for this prop based on shouldForwardProp.
Was the as prop implemented? Is there any documentation about it?
Yes, its implemented. https://emotion.sh/docs/styled#as-prop
Most helpful comment
It actually can be implemented user land with smth like:
Which is better than a @alexkrautmann version as it doesn't recreate new component types in render.
As to the component flattening - we do the same in emotion, but for
asit doesn't quite matter (nor it does in styled-components case).The only open question for us is how should we handle
shouldForwardPropin combination withas.