Skip to content

Commit

Permalink
feat: tw.apply for component like styles (#83)
Browse files Browse the repository at this point in the history
* feat: tw.apply for component like styles

* docs: adding tw.apply docs

* fix: upstream changes

* chore: format

* doc: update size

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update README.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* fix: ensure tw.apply works as CSS value and within preflight

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* Update docs/components.md

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>

* chore: docs

* fix: deep eval functions

Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com>
  • Loading branch information
sastan and rschristian committed Jan 17, 2021
1 parent ffe3f55 commit d00a9d2
Show file tree
Hide file tree
Showing 21 changed files with 1,000 additions and 71 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [Grouping](https://github.com/tw-in-js/twind/tree/main/docs/grouping.md) - how to optimize rules size
- [Tailwind Extensions](https://github.com/tw-in-js/twind/tree/main/docs/tailwind-extensions.md) - which additional features are available
- [CSS-in-JS](https://github.com/tw-in-js/twind/tree/main/docs/css-in-js.md) - how to apply custom css
- [Components](https://github.com/tw-in-js/twind/tree/main/docs/components.md) - how to define component styles
- [Plugins](https://github.com/tw-in-js/twind/tree/main/docs/plugins.md) - how to extend twind
- [Testing](https://github.com/tw-in-js/twind/tree/main/docs/sheets.md) - how to verify the generated class names
- [Static Extraction (SSR)](https://github.com/tw-in-js/twind/tree/main/docs/ssr.md) - how to extract the generated css on the server
Expand Down Expand Up @@ -154,9 +155,9 @@ It might not always be desirable to generate rules by invoking the compiler dire
</details>

<details><summary>💸 Unlimited styles for a low fixed cost of ~11KB</summary>
<details><summary>💸 Unlimited styles for a low fixed cost of ~12KB</summary>

By shipping the compiler (rather than the resultant output) there is a known and fixed cost associated with styling. No matter how many styles you write or how many variants you use, all your users will ever have to download is approximately 11Kb of code (which is less than styled-components or your average purged Tailwind build).
By shipping the compiler (rather than the resultant output) there is a known and fixed cost associated with styling. No matter how many styles you write or how many variants you use, all that your users will ever have to download is approximately 12Kb of code (which is less than styled-components or your average purged Tailwind build).

</details>

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ If you find any incorrect or missing documentation then please [open an issue](h
- [Grouping](./grouping.md) - how to optimize rules size
- [Tailwind Extensions](./tailwind-extensions.md) - which additional features are available
- [CSS-in-JS](./css-in-js.md) - how to apply custom css
- [Components](./components.md) - how to define component styles
- [Plugins](./plugins.md) - how to extend twind
- [Testing](./sheets.md) - how to verify the generated class names
- [Static Extraction (SSR)](./ssr.md) - how to extract the generated css on the server
Expand Down
305 changes: 305 additions & 0 deletions docs/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
# Defining Components

As a component author, one often wants to re-use Tailwind directive styles for defining a component and allow users of the component to override styles using Tailwind rules. The created component can be used as a base for child components and override or add some styles using Tailwind rules.

`tw.apply` generates one style object, e.g., one CSS class, combining all Tailwind rules by deep merging rules in order of declaration.

```jsx
const btn = tw.apply`inline-block bg-gray-500 text-base`
// => generates on CSS class with all declarations of the above rules when used

const btnBlock = tw.apply`${btn} block`
// => generates on CSS class with all declarations of btn & block

<button class={tw`${btn}`}>gray-500</button>
// => tw-XXXXX

<button class={tw`${btn} bg-red-500 text-lg`}>red-500 large</button>
// => tw-XXXX bg-red-500 text-lg

<button class={tw`${btnBlock}`}>block button</button>
// => tw-YYYY
```

> Another way to extract common component styles is by [using an alias plugin](./plugins.md#plugin-as-alias).
The component styles are added **before** the utility classes to the stylesheet which allows utilities to override component styles.

<details><summary>Why can I not use <code>tw</code> for components?</summary>

```jsx
const Button = ({ className, children}) => {
return <button className={tw`inline-block bg-gray-500 text-base ${className}`}>{children}</button>
}

const ButtonBlock = ({ className, children}) => {
return <Button className={`block ${className}`}>{children}</Button>
}

<Button>gray-500</Button>
<Button className="bg-red-500 text-lg">red-500 large</Button>
```

The example above does not reliably work because the injected CSS classes have all the same specificity and therefore the order in which they appear in the stylesheet determines which styles are applied.

It is really difficult to know which directive does override another. For now, let's stick with `bg-*` but there are others. The `bg` prefix and its plugin handle several CSS properties where `background-color` is only one of them.

- `background-color`: `bg-current`, `bg-gray-50`, ... (see https://tailwindcss.com/docs/background-color)
- `background-attachment`: `bg-local`, ... (see https://tailwindcss.com/docs/background-attachment)
- `--tw-bg-opacity`: `bg-opacity-10`, ... (see https://tailwindcss.com/docs/background-opacity)
- and a lot more
- not to forget about user plugins and inline directives

This ambiguity makes class based composition really difficult. That was the reason we introduced the `override` variant.

Consider the following example:

```js
const Button = tw`
text(base blue-600)
rounded-sm
border(& solid 2 blue-600)
m-4 py-1 px-4
`

// Create a child component overriding some colors
const PurpleButton = tw`
${Button}
override:(text-purple-600 border-purple-600)
`
```

As you see it is difficult to override certain utility classes on usage or when creating a child component. For this to work twind introduced the `override` variant which increases the specificity of the classes it is applied to. But what do you do for a grandchild component or if you want to override the `PurpleButton` styles? `override:override:...`? This is where `tw.apply` should be used.

Tailwind has a component concept using [@apply](https://tailwindcss.com/docs/extracting-components#extracting-component-classes-with-apply) which basically merges the CSS rules of several Tailwind classes into one class. twin.macro does the same.

<details><summary>Details of Tailwind @apply</summary>

Tailwind CSS provides [@apply to extract component classes](https://tailwindcss.com/docs/extracting-components#extracting-component-classes-with-apply) which merges the underlying styles of the utility classes into a single CSS class.

```css
.btn-indigo {
@apply py-2 px-4 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75;
}
```

[twin.macro](https://github.com/ben-rogerson/twin.macro) does the same during build time to generate CSS-in-JS objects which are evaluated with a runtime like Emotion or styled-components:

```js
const hoverStyles = css`
&:hover {
border-color: black;
${tw`text-black`}
}
`
const Input = ({ hasHover }) => <input css={[tw`border`, hasHover && hoverStyles]} />
```

> The `tw` function from `twin.macro` acts like the `@apply` helper from Tailwind CSS.
</details>

</details>

## API

> `tw.apply` accepts the same arguments as [tw](./tw.md#function-signature).
```js
import { tw } from 'twind'

const btn = tw.apply`
py-2 px-4
font-semibold
rounded-lg shadow-md
focus:(outline-none ring(2 indigo-400 opacity-75))
`

tw`${btn} font-bold`
// => .tw-btn .font-bold
// CSS:
// .tw-XXXX { padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; padding-right: 1rem; font-weight: 600; ...}
// .font-bold { font-weight: 700; }

const btnLarge = tw.apply`${btn} py-4 px-8`
// Result: () => ({ paddingTop: '1rem', paddingBottom: '1rem', paddingLeft: '2rem', paddingRight: '2rem', fontWeight: '600', ... })

tw`${btnLarge} rounded-md`
// => .tw-btn-large .rounded-md
// CSS:
// .tw-btn-large { padding-top: 1rem; padding-bottom: 1rem; padding-left: 2rem; padding-right: 2rem; font-weight: 600; ... }
// .rounded-md { ... }
```

The returned function has `toString` and `valueOf` methods which inject the styles and return the class name without the need to pass theme to `tw`:

```jsx
;<button className={tw.apply`bg-red bg-blue`}>blue</button>
// => tw-red-blue

document.body.className = tw.apply`bg-blue bg-red`
// => tw-blue-red
```

Or use this helper:

```jsx
// There is a better name out there somewhere
const twind = (...args) => tw(tw.apply(...args))

<button className={twind`bg-red bg-blue`}>blue</button>
// => tw-red-blue

document.body.className = twind`bg-blue bg-red`
// => tw-blue-red
```

## Examples

<details><summary>Using <code>tw.apply</code> within <code>preflight</code></summary>

Use Tailwind rules within [preflight](./setup.md#preflight).

```js
setup({
preflight: {
body: tw.apply('bg-gray-900 text-white'),
},
})
```

</details>

<details><summary><code>CSS</code> can be used within <code>tw.apply</code></summary>

[twind/css](./css-in-js.md) can be used to define additional styles.

```js
const btn = tw.apply`
py-2 px-4
${css({
borderColor: 'black',
})}
`
```

</details>

<details><summary>Using within <code>CSS</code> – pending</summary>

`tw.apply` can be used with `css` (_pending variable arguments, array support_):

```js
const prose = css(
tw.apply`text-gray-700 dark:text-gray-300`,
{
p: tw.apply`my-5`,
h1: tw.apply`text-black dark:text-white`,
},
{
h1: {
fontWeight: '800',
fontSize: '2.25em',
marginTop: '0',
marginBottom: '0.8888889em',
lineHeight: '1.1111111',
},
},
)
```

Using template literal syntax (_pending, but I'm working on it_):

```js
const prose = css`
${tw.apply`text-gray-700 dark:text-gray-300`)
p { ${tw.apply('my-5')} }
h1 {
${tw.apply`text-black dark:text-white`}
font-weight: 800;
font-size: 2.25em;
margin-top: 0;
margin-bottom: 0.8888889em;
line-height: 1.1111111;
}
`
```
</details>
<details><summary>Using Tailwind directives with <code>animation</code> from <code>twind/css</code></summary>
```js
const motion = animation('.6s ease-in-out infinite', {
'0%': tw.apply`scale-100`,
'50%': tw.apply`scale-125 rotate-45`,
'100%': tw.apply`scale-100 rotate-0`,
})
```
</details>
<details><summary>A React button component</summary>
```jsx
import { tw } from 'twind'
const variantMap = {
success: 'green',
primary: 'blue',
warning: 'yellow',
info: 'gray',
danger: 'red',
}
const sizeMap = {
sm: tw.apply`text-xs py(2 md:1) px-2`,
md: tw.apply`text-sm py(3 md:2) px-2`,
lg: tw.apply`text-lg py-2 px-4`,
xl: tw.apply`text-xl py-3 px-6`,
}
const baseStyles = tw.apply`
w(full md:auto)
text(sm white uppercase)
px-4
border-none
transition-colors
duration-300
`
function Button({
size = 'md',
variant = 'primary',
round = false,
disabled = false,
className,
children,
}) {
// Collect all styles into one class
const instanceStyles = tw.apply`
${baseStyles}
bg-${variantMap[variant]}(600 700(hover:& focus:&)))
${sizeMap[size]}
rounded-${round ? 'full' : 'lg'}
${disabled && 'bg-gray-400 text-gray-100 cursor-not-allowed'}
`
// Allow passed classNames to override instance styles
return <button className={tw(instanceStyles, className)}>{children}</button>
}
render(
<Button variant="info" className="text-lg rounded-md">
Click me
</Button>,
)
```
</details>
<hr/>
Continue to [Plugins](./plugins.md)
2 changes: 1 addition & 1 deletion docs/css-in-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,4 @@ slidein({ tw })

<hr/>

Continue to [Plugins](./plugins.md)
Continue to [Defining Components](./components.md)
2 changes: 1 addition & 1 deletion docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Theming and customization lets you specify how core plugins and the compiler beh
- [Plugins without arguments](#plugins-without-arguments)
- [Plugins with arguments](#plugins-with-arguments)
- [Referencing the theme](#referencing-the-theme)
- [Inject global styles](#inject-global-styles)
- [Inline Plugins](#inline-plugins)
- [Inject global styles](#inject-global-styles)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
</details>
Expand Down
10 changes: 10 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ To smooth over browser inconsistencies, Tailwind provide a [opinionated modern r
})
```

- [apply](./components.md) Tailwind rules

```js
setup({
preflight: {
body: tw.apply('bg-gray-900 text-white'),
},
})
```

## Mode

One benefit of doing compilation at runtime is that it is possible to warn developers about errors such as:
Expand Down
1 change: 1 addition & 0 deletions example/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../src/__tests__/api.test'
import '../src/__tests__/apply.test'
import '../src/__tests__/dark-mode.test'
import '../src/__tests__/hash.test'
import '../src/__tests__/mode.test'
Expand Down

0 comments on commit d00a9d2

Please sign in to comment.