Skip to content

Commit

Permalink
feat: allow interpolating components (#267)
Browse files Browse the repository at this point in the history
* feat: allow interpolating components

* WIP

* support css tag

* add composes support

* Apply suggestions from code review

Co-Authored-By: Jimmy Jia <tesrin@gmail.com>

* Update src/utils/buildTaggedTemplate.js

Co-Authored-By: Jimmy Jia <tesrin@gmail.com>

* Update src/utils/buildTaggedTemplate.js

Co-Authored-By: Jimmy Jia <tesrin@gmail.com>
  • Loading branch information
jquense and taion authored Jul 5, 2019
1 parent d75db1c commit 05f73f2
Show file tree
Hide file tree
Showing 16 changed files with 698 additions and 118 deletions.
106 changes: 78 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
- [Usage](#usage)
- [Extensions](#extensions)
- [Component API](#component-api)
- [WHY?!](#why)
- [Component API Goals and Non-Goals](#component-api-goals-and-non-goals)
- [Composition, variables, etc?](#composition-variables-etc)
- [Referring to other Components](#referring-to-other-components)
- [Sharing values between styles and JavaScript](#sharing-values-between-styles-and-javascript)
- [Keyframes and global](#keyframes-and-global)
- [Helpers](#helpers)
- [Attaching Additional Props](#attaching-additional-props)
- [`as` prop](#as-prop)
- [Setup](#setup)
- [Options](#options)
- [Use with Next.js](#use-with-next.js)
- [Use with Parcel](#use-with-parcel)
- [Use with Preact](#use-with-preact)
- [Use with Next.js](#use-with-nextjs)
- [Use without webpack](#use-without-webpack)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -75,7 +78,7 @@ const styles = css`

## Component API

For those that want something a bit more like styled-components or emotion, there is a component API!
For those that want something a bit more like styled-components or Emotion, there is a component API!

```js
import styled, { css } from 'astroturf';
Expand Down Expand Up @@ -150,15 +153,15 @@ There are a whole bucket of caveats of course, to keep the above statically extr
- Prop value handling requires the nesting transform
- All "top level" styles have any @import statements hoisted up (via a regex)

### WHY?!
### Component API Goals and Non-Goals

The goal of this API is not to mimic or reimplement the features of other css-in-js libraries, but to provide
a more ergonomic way to write normal css/less/sass next to your javascript.

What does that mean? css-in-js libraries are often a _replacement_ for css preprocessors, in that they provide ways of doing variables, composition, mixins, imports etc. Usually they accomplish this by leaning
What does that mean? css-in-js libraries are a _replacement_ for css preprocessors, in that they provide ways of doing variables, composition, mixins, imports etc. Usually they accomplish this by leaning
on JS language features where appropriate, and adding their own domain-specific language bits when needed.

astroturf **doesn't try to do any of that** because it's not trying to replace preprocessors but rather, make component-centric javascript work better with **existing** styling tooling. This means at a minimum it needs to scope styles to the component (handled by css-modules) and map those styles to your component's API (props), which is what the above API strives for.
astroturf **doesn't try to do any of that** because it's not trying to replace preprocessors, but rather, make component-centric javascript work better with **existing** styling tooling. This means at a minimum it needs to scope styles to the component (handled by css-modules) and map those styles to your component's API (props), which is what the above API strives for.

This approach **gains** us:

Expand All @@ -175,7 +178,7 @@ It also means we **sacrifice**:

### Composition, variables, etc?

How you accomplish that is mostly up to your preprocessor. Leverage Sass variables, or Less mixins, or postcss nesting polyfills, or whatever. The css you're writing is treated exactly like a normal style file so all the tooling you're used to works as expected. For composition, specifically around classes, you can also use css-modules `composes` to compose styles, since astroturf extracts styles to consistent names;
How you accomplish that is mostly up to your preprocessor. Leverage Sass variables, or Less mixins, or postcss nesting polyfills, or whatever. The css you're writing is treated exactly like a normal style file so all the tooling you're used to works as expected. For composition, specifically around classes, you can also use css-modules `composes` to compose styles and interpolation;

```js
// Button.js
Expand All @@ -187,7 +190,7 @@ const helpers = css`
`;

const Title = styled('h3')`
composes: heavy from './Button-helpers.css';
composes: ${helpers.heavy};
font-size: 12%;
`;
Expand All @@ -214,6 +217,35 @@ const Title = styled('h3')`
`;
```

### Referring to other Components

One limitation to fully encapsulated styles is that it's hard to contextually style components
without them referencing each other. In astroturf you can use a component in a
selector as if it were referencing a class selector.

> Note: at the moment this only works for local identifiers, not imported components
```js
const Link = styled.a`
display: flex;
align-items: center;
padding: 5px 10px;
background: papayawhip;
color: palevioletred;
`;

const Icon = styled.svg`
flex: none;
transition: fill 0.25s;
width: 48px;
height: 48px;
${Link}:hover & {
fill: rebeccapurple;
}
`;
```

### Sharing values between styles and JavaScript

We've found that in practice, you rarely have to share values between the two, but there are times when it's
Expand Down Expand Up @@ -315,31 +347,49 @@ const Loader = styled('div')`
`;
```

### Helpers
### Attaching Additional Props

A common task with styled components is to configure or map their props. We include a few helpers you can
optionally include to do this if you want, they are extra and if you don't use them they won't be included in your bundle. There are a few advantages to using the included helpers over a more general solution
like `recompose`. They automatically forward `refs`, and don't muck with the `as` prop passthrough.
A common task with styled components is to map their props or set default values.
astroturf cribs from Styled Components, by including an `attrs()` api.

```jsx
import styled from 'astroturf';
import { withProps, defaultProps, mapProps } from 'astroturf/helpers';

// Map the incoming props to a new set of props
const TextInput = mapProps(props => ({ ...props, type: 'password' }))(
styled('input')`
background-color: #ccc;
`,
);
// Provide a default `type` props
const PasswordInput = styled('input').attrs({
type: 'password',
})`
background-color: #ccc;
`;

// Provides `type` automatically and passes through everything else
const PasswordInput = withProps({ type: 'password' })(styled('input')`
// Map the incoming props to a new set of props
const TextOrPasswordInput = styled('input').attrs(
({ isPassword, ...props }) => ({
...props,
type: isPassword ? 'password' : 'text',
}),
)`
background-color: #ccc;
`;
```

Because `attrs()` is resolved during render you can use hooks in them! We even
do some magic in the non-function signature so that it works.

```js
const Link = styled('a').attrs(props => ({
href: useRouter().createHref(props.to)
}))`
color: blue;
`);

// Sets the default `type` to `text` but allow overrides to it
const TextInput = withProps({ type: 'text' })(styled('input')`
background-color: #ccc;
// astroturf will automatically compile to a function
// when using a plain object so that the hooks
// are only evaluated during render
const Link = styled(MyLink).attrs({
router: useRouter()
})`
color: blue;
`);
```

Expand All @@ -360,7 +410,7 @@ const Button = styled('button')`
```js
const StyledFooter = styled(Footer, { allowAs: true })`
color: red;
`
`;
```

## Setup
Expand All @@ -384,7 +434,7 @@ If you want the simplest, most bare-bones setup you can use the included `css-lo
test: /\.tsx?$/,
use: ['ts-loader', 'astroturf/loader'],
},
]
];
}
}
```
Expand All @@ -396,7 +446,7 @@ You can add on here as you would normally for additional preprocesser setup. Her
module: {
rules: [
{
test: /\module\.scss$/,
test: /\.module\.scss$/,
use: ['style-loader', 'astroturf/css-loader', 'sass-loader'],
},
{
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"loader-utils": "^1.2.3",
"lodash": "^4.17.11",
"memory-fs": "^0.4.1",
"css-loader": "^3.0.0",
"postcss-loader": "^3.0.0",
"postcss-nested": "^4.1.1"
},
Expand All @@ -82,9 +83,8 @@
"babel-core": "^7.0.0-0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"babel-loader": "^8.0.5",
"babel-loader": "^8.0.6",
"cpy-cli": "^2.0.0",
"css-loader": "^2.1.0",
"doctoc": "^1.4.0",
"dtslint": "^0.8.0",
"enzyme": "^3.9.0",
Expand Down
58 changes: 32 additions & 26 deletions src/css-loader.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
module.exports = c => c;
const cssLoader = require('css-loader');
const postcssLoader = require('postcss-loader');
const postcssNested = require('postcss-nested');

/**
* A loader that replaces itself with css-loader and postcss-loader
*/
module.exports.pitch = function pitch() {
const remaining = this.loaders.slice(this.loaderIndex + 1);
this.loaders = [
{
path: require.resolve('css-loader'),
query: '',
options: {
...this.query,
modules: true,
importLoaders: remaining.length ? 2 : 1,
},
},
{
path: require.resolve('postcss-loader'),
query: '',
options: {
ident: 'postcss-astroturf',
plugins: [require('postcss-nested')()], // eslint-disable-line global-require
},
},
...remaining,
];
function postcss(loader, css, map, meta, cb) {
const ctx = { ...loader };
ctx.async = () => cb;
ctx.loaderIndex++;
ctx.query = {
plugins: [postcssNested()],
};

postcssLoader.call(ctx, css, map, meta);
}

module.exports = function loader(css, map, meta) {
const done = this.async();

postcss(this, css, map, meta, (err, ...args) => {
if (err) {
done(err);
return;
}

const ctx = { ...this };
ctx.query = {
...this.query,
modules: this.query.modules || true,
importLoaders: 0,
};

cssLoader.call(ctx, ...args);
});
};
2 changes: 1 addition & 1 deletion src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function collectStyles(src, filename, opts) {
// quick regex as an optimization to avoid parsing each file
if (
!src.match(
new RegExp(
new RegExp(
`(${tagName}|${styledTag}(.|\\n|\\r)+?)\\s*\`([\\s\\S]*?)\``,
'gmi',
),
Expand Down
Loading

0 comments on commit 05f73f2

Please sign in to comment.