Hi!
I have a problem with string interpolation in tw macro.
For example I need to customize border color, so I'm trying to do it like this:
export const PrimaryButton = styled(Button)`
${({ color }) => tw`hover:border-${color ?? 'blue'}`};
`;
But I get the following error:
✕ Class “hover:border-” shouldn’t have a trailing dash.
It also doesn't work without variables:
export const PrimaryButton = styled(Button)`
${({ color }) => tw`hover:border-${'blue'}`};
`;
I've run into this issue before. I think the only solution is to do something like this instead:
export const PrimaryButton = styled(Button)`
${({ color }) => color === 'blue' ? tw`hover:border-blue-400` : tw`hover:border-gray-400`};
`;
or you could do something like this
const generateBorderColor = (color) {
switch(color) {
case 'blue':
return tw`hover:border-blue-400`
default:
return tw`hover:border-gray-400`
}
}
const PrimaryButton = styled(Button)`
${({color}) => ${generateBorderColor(color}}}
`
@rbutera Thanks for suggestions.
Unfortunately, it's very inflexible solution, because we have lots of colors.
I wonder if we could make it work properly with interpolation.
I think everyone comes across this limitation sooner or later.
Edit: Check below for examples.
@ben-rogerson I think it's not a babel-plugin-macros issue, because I couldn't find any such issue in their repo.
Also I see that it works properly here: https://github.com/vinhlh/tagged-translations#via-babel-plugin-macros
And maybe if function calls like tw() worked we wouldn't have this problem?
Interesting, I think that's worth looking into for sure.
Personally this would be a huge benefit for using this plugin. I did take a look, however, my knowledge of writing babel plugins/macros is somewhat slim.
I would imagine this is a small change. It might be worth looking at how emotion { css } prop works, as they have a function that get utilised via a macro (css) but is still able to be used as a function... super super cool.
Something like this in terms of utilising it.
import tw from 'twin.macro'
import { css } from '@emotion/core'
const test1 = (...args) =>
css`
${css(...args)}
`
console.log('test1', test1({ padding: 0 }))
const test2 = (...args) =>
css`
${tw(...args)}
`
console.log('test2', test2(`text-lg px-8 py-2 rounded
hocus:transform hocus:scale-105 focus:outline-none
hocus:transition-transform hocus:duration-75`))
Update: @ben-rogerson @semirart suggests https://github.com/vinhlh/tagged-translations#via-babel-plugin-macros having played with this ... so for quickness. I installed the tagged-translations macro .... added a folder to cra app called translations/default.json added {
"%s": "%s"
}
import t from 'tagged-translations/macro';
export const styles = (ts) => t`${ts}`;
console.log(styles(`text-red-100 hover:bg-blue-200`))
in another file...
import React from 'react';
import { styles } from "./App";
export default () => <div>cool{styles(`text-red-100 hover:bg-blue-200`)}</div>;
and the output...
matched the correct string 'text-red-100 hover:bg-blue-200' so dynamic passing of styles works which could mean way more flexibility. This would mean library authors could use this in many different ways.
thanks for the bump on this - I've taken a look into the tagged translations output.
In this test you can see that t doesn't support being used in a function:
// In
import t from "tagged-translations/macro";
const styles = ts => t`${ts}`;
styles(`text-red-100 hover:bg-blue-200`);
// Out
const styles = ts => `${ts}`;
styles(`text-red-100 hover:bg-blue-200`);
I'm going to look at how the css prop can be used as a macro, hopefully that leads to something.
I've taken a look at Emotions css macro and it's switching the css macro out with their runtime library when babel runs:
// In
import css from "@emotion/css/macro";
const test = (...args) => css(...args);
// Out
import _css from "@emotion/css";
const test = (...args) => _css(...args);
and if you're using it with css (I've stripped away their dev stuff):
// In
import css from "@emotion/css/macro";
css`color: black`;
// Out
import _css from "@emotion/css";
({
name: "1xu3tth",
styles: "color:black;"
});
Twin doesn't have a runtime (it compiles away when Babel runs) so this approach isn't a possibility.
Here are the same tests with Twin:
// In
import tw from "twin.macro";
const test = (...args) => tw(...args);
// Out
const test = (...args) => tw(...args);
and with a class:
// In
import tw from "twin.macro";
tw`text-black`;
// Out
({
"color": "#000"
});
I'm open to further ideas, keeping in mind there are no plans to create a tw runtime.
thanks for the bump on this - I've taken a look into the tagged translations output.
In this test you can see thattdoesn't support being used in a function:// In import t from "tagged-translations/macro"; const styles = ts => t`${ts}`; styles(`text-red-100 hover:bg-blue-200`); // Out const styles = ts => `${ts}`; styles(`text-red-100 hover:bg-blue-200`);I'm going to look at how the css prop can be used as a macro, hopefully that leads to something.
Thanks for replying.
example.js
import React from "react";
import { styles } from "./translate";
export default () => (
<>
<div>cool{styles(`text-red-100 hover:bg-blue-200`, `200`)}</div>
<div>cool{styles(`text-blue-100 hover:bg-orange-400`, `600`)}</div>
</>
);
translate.js
import t from 'tagged-translations/macro';
export const styles = (ts, other) => t`${ts} text-red-${other}`;
All i'm saying is the text is replaced... with the correct value here making the strings dynamic, allowing me to re-use the tw in a single place.
Maybe i'm missing the point. But at the moment how would you support theming using this type of macro? For me id like to pass in dynamic tags to tw to make it something a bit more dynamic otherwise i may as-well just include tailwind manually and use their classnames. Im not saying this isn't a good macro as i can see its benefits when using it in for example emotions css selector.
The only thing is that if i built a ui library using tw.macro (maybe not the right tool) then how when in the project that is using these components would you style (override) the components. You would have to include tw macro in the project as well has the lib to utilise tailwind as when you ship the ready made component the css will have been generated upfront by the macro.
No idea if i'm making any sense, maybe styled-system is a better approach for this :-)
... when you ship the ready made component the css will have been generated upfront by the macro.
Yeah, your library components should be pre-generated.
There's a couple of ways you could then restyle your lib components (but I'm not saying it's pretty 😄 ):
import { List, ListItem } from 'mylib'
const RestyledList = tw(List)`mt-3`
const MyList = (
<RestyledList>
<ListItem tw="text-red-500">Item</ListItem>
</RestyledList>
)
I understand you're seeking a more dynamic way of passing Tailwind through but I currently don't know how that would be possible with Twin.
I think everyone comes across this limitation sooner or later.
~It's because babel macros (the plugin twin uses) run before the interpolation occurs.~
Edit: Interpolation coming soon.
Hey @ben-rogerson, is interpolation still coming / do you have a solution for it?
If yes, is there a way to help?
hey Nico, no solution yet.
I had a crack at it a week ago but didn't get too far.
Help is welcomed for sure - I believe this is where to start.
Previous experience with Babel plugins or traversing ASTs is a plus 😃
Edit: Not sure if this is useful, but replacing that line with this:
const string = path.get('quasi').evaluate().value
Will allow you to pass a plain variable into the backticks but it's quite limited. It can't be conditional or a function:
const className = 'flex';
tw`${className}`
Hi @ben-rogerson, thanks for looking into this. Interpolation support would be a game-changer for flexibility, even if just with a basic variable like the example you show above. Is this planned for inclusion soon, or is it better to fork for now?
PS. Twin is probably the tool that brings me the most developer happiness at the moment. :) Thanks for all your hard work!
If you can fork it and implement this ai think I speak on behalf of everyone and please submit a PR, Johannes 😃
Sent via Superhuman iOS ( https://sprh.mn/[email protected] )
On Sun, May 3 2020 at 1:29 pm, Johannes Jonker < [email protected] > wrote:
Hi @ben-rogerson ( https://github.com/ben-rogerson ) , thanks for looking
into this. Interpolation support would be a game-changer for flexibility,
even if just with a basic variable like the example you show above. Is
this planned for inclusion soon, or is it better to fork for now?PS. Twin is probably the tool that brings me the most developer happiness
at the moment. :) Thanks for all your hard work!—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub (
https://github.com/ben-rogerson/twin.macro/issues/17#issuecomment-623102784
) , or unsubscribe (
https://github.com/notifications/unsubscribe-auth/ABOEMPPYMU2VIFI6D2JMC33RPVPUPANCNFSM4LST4SPQ
).
Hi @rbutera, I was just referring to replacing the line like Ben suggested above. Unfortunately I don’t have any other insight into solving the issue. :(
Happy to try submit a PR changing the line as per Ben’s suggestion if he prefers that?
@ben-rogerson I tested out the patch you suggested.
Edit: Not sure if this is useful, but replacing that line with this:
const string = path.get('quasi').evaluate().valueWill allow you to pass a plain variable into the backticks but it's quite limited. It can't be conditional or a function:
const className = 'flex'; tw`${className}`
I thought I'd add a further data point. As you say, it works for simple variables. It doesn't seem to work for this use case, though:
const { styles } = config.button; // = 'inline-block font-semibold rounded tracking-wider border border-transparent`
const styles2 = `inline-block font-semibold rounded tracking-wider border border-transparent`;
console.log(styles === styles2); // = true
console.log(tw`${styles2}`); // = {display: "inline-block", fontWeight: "600", borderRadius: "0.25rem", letterSpacing: "0.05em", borderWidth: "1px", ...}
console.log(tw`${style}`); // bombs out
I have some predefined styles stored as raw Tailwind classes (styles). The value hereof is equivalent to a simple variable definition (styles2). However, when I use this with the macro, it bombs out with:
twin.macro: Cannot read property 'match' of undefined
I can't figure out exactly where this is happening in the plugin, but the 'match' function call is only present in these locations:
/src/getStyles.js:20
const classList = string.match(/\S+/g) || []
/src/utils.js:129
const match = x.selector.match(/^\.(\S+)(\s+.*?)?$/)
/src/variants.js:87
modifier = className.match(/^([_a-z-]+):/)
Any ideas as to why the string/selector/className would be coming through undefined, and how to add a workaround?
Thanks for your help! Unfortunately, I'm not familiar with Babel Macros, otherwise I could've been of more use.
I think it may be happening in /src/getStyles.js:20and it's failing because the string isn't being interpolated and getting passed as undefined.
I'll have a new version out shortly and I'll take another look into it shortly afterwards.
Edit: I've added basic interpolations in v1.0.0
I've run into this issue before. I think the only solution is to do something like this instead:
export const PrimaryButton = styled(Button)` ${({ color }) => color === 'blue' ? tw`hover:border-blue-400` : tw`hover:border-gray-400`}; `;or you could do something like this
const generateBorderColor = (color) { switch(color) { case 'blue': return tw`hover:border-blue-400` default: return tw`hover:border-gray-400` } } const PrimaryButton = styled(Button)` ${({color}) => ${generateBorderColor(color}}} `
Hi everyone, I got stuck with this issue too, the switch approach is good enough if we have only a few colors, but what if 20+ colors, or even 30+? The code will be super long and incompatible with reusing logic.
You could try a style map to reuse the styleMap object:
const styleMap = {
primary: tw`bg-black text-white`,
secondary: tw`bg-white text-black`,
default: tw`bg-gray-500`
}
const getStyleName = ({ name }) => styleMap[name] || styleMap.default
const Button = styled.button(getStyleName)
const Buttons = () => (
<>
<Button name="primary">Primary</Button>
<Button name="secondary">Secondary</Button>
<Button>Default</Button>
</>
)
You could also separate the styles and import them from another file:
// styles.js
const primary = tw`bg-black text-white`
const secondary = tw`bg-white text-black`
const other = tw`bg-gray-500`
const stylesCombined = { primary, secondary }
const getStyleName = ({ name }) => stylesCombined[name] || other
export default getStyleName
// Button.js
import getStyleName from './styles'
const Button = styled.button(getStyleName)
const Buttons = () => (
<>
<Button name="primary">Primary</Button>
<Button name="secondary">Secondary</Button>
<Button>Default</Button>
</>
)
I remember when working with Vue, I could apply Tailwind classes to the class attribute of a custom component, and they would automatically get appended to the component's root element, creating a really powerful override workflow that sat really nicely with Tailwind.
I'm new to both React and twin.macro, so please correct me if I'm wrong - if I wanted a similar workflow using className I'd need to explicitly pass the prop down and use string interpolation to add the passed down className prop to the component's root element's className.
Am I correct in thinking that this string interpolation issue prevents a similar overriding workflow being possible with twin.macro's tw prop?
@alexhillel There are decent ways around interpolating classes, take a look at this codesandbox that shows different ways your styles could be assembled.
I know this isn't a great solution but one possible solution I've used to get around this for now is doing something like:
const Container = styled.div`${tw`-mx-8 -mb-8`}`;
const Component = ({
background = 'gray-200',
color = 'gray-700',
}: Props) => (
<Container className={`bg-${background} text-${color}`}>
Text Here
</Container>
)
Where the dynamic tailwind options are passed as props to className.
This is especially useful when using padding in the tailwind macro - eg: gap.
declare const gap: number
const styles = tw`px-${gap}`
I'm not sure whats the best workaround currently.
This is especially useful when using padding in the tailwind macro - eg:
gap.declare const gap: number const styles = tw`px-${gap}`I'm not sure whats the best workaround currently.
Interpolation isn't possible with Babel macros so try using something like this:
const gapSize = size => ({
small: tw`px-2`,
large: tw`px-4`,
}[size]);
const smallGap = gapSize('small')
@ben-rogerson yep that's what I ended up doing, it's just a shame because I needed to also accomodate for sm, md, lg and xl of all the sizes, and my gapSize objects ended up being over 225 lines of code - but I don't think there's any other option so it's fine for now
I documented all the spacings in Storybook and therefore stumbled upon the same issues.
This does not work:
const { spacing } = useTheme();
Object.entries(spacing).map(([key, value]) =>
<div key={key}>
<div>{key}</div>
<div tw={`px-${key}`}>...</div> // error, no interpolation allowed
</div>
);
I fixed it by using the 'raw' spacing values from the tailwind config instead of using tw:
<div css={{ paddingRight: value, paddingLeft: value }}>...</div>
Worth noting is that this won't get the combined config values, only the ones you define in your tailwind.config.js.
Perhaps the theme import could be updated to grab the entire combined theme if you leave off the backticks. Currently, the theme import only grabs a single "combined theme" value.
eg:
// concept only
import { theme } from 'twin.macro'
Object.keys(theme.spacing).map(key =>
<div key={key}>
<div tw={`px-${key}`}>...</div>
</div>
);
Interpolation isn't possible with Babel macros so try using something like this:
const gapSize = size => ({ small: tw`px-2`, large: tw`px-4`, }[size]); const smallGap = gapSize('small')
can we simplify it like
const gapSize = {
small: tw`px-2`,
large: tw`px-4`,
};
console.log(gapSize['small'])
it will return the same
@wahyupriadi Sure - that works too 👍 You just need use tw alongside the value.
EDIT: I previously asked whether TypeScript unions could be used to generate simple interpolated values with a runtime selector. Expand to see the suggestion.
Naive question, and possibly not constructive, from someone without twin experience: Could TS types be used (and thus required) for interpolation? Eg;
// lib:
module TW {
type Color = 'blue' | 'red' | ...;
}
// in:
import {TW} from 'twin.macro';
export const PrimaryButton = styled(Button)`
${({ color }: { color: TW.Color }) => tw`border-${color ?? 'blue'}`};
`;
// out:
export const PrimaryButton = styled(Button)(color => ({
border: `1px solid ${{ blue: '#00F', red: '#F00' }[color]}`
}));
Of course, since Babel does not have access to TS's toolchain during compilation, the union types used would have to be quite simplistic, like only unions coming directly from twin, or defined as a single literal within the file. Anything "fancy" like type AllowedColors = Exclude<TW.Color, 'orange'> or a shared union of colors defined in another file, wouldn't work – and I can definitely imagine design system engineers really wanting those.
Ultimately, my conclusion is that ~any interpolation scheme would probably cause a surprising amount of code bloat.
It's thus probably better to rely on raw css or style attributes if you want truly dynamic "pass whatever you want" styling, and switch statements or equivalent when the options used in practice are limited to a handful.
@rattrayalex Twin doesn't know how to work with TS interpolation like that, perhaps take a look at these guides I'm working on for all the ways you can use conditional styling
Most helpful comment
I think it may be happening in
/src/getStyles.js:20and it's failing because the string isn't being interpolated and getting passed asundefined.I'll have a new version out shortly and I'll take another look into it shortly afterwards.
Edit: I've added basic interpolations in v1.0.0