Skip to content

Commit 032ce9b

Browse files
author
rofrischmann
committed
initial version 1.0.0
1 parent 3702c76 commit 032ce9b

17 files changed

+5219
-2
lines changed

.babelrc

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"presets": [
3+
"react",
4+
"stage-0",
5+
"flow"
6+
]
7+
}

.codeclimate.yml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
languages:
2+
Ruby: true
3+
JavaScript: true
4+
PHP: true
5+
Python: true
6+
exclude_paths:
7+
- "src/__tests__/**/*"

.eslintrc

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"extends": [ "airbnb" ],
3+
"parser": "babel-eslint",
4+
"env": {
5+
"browser": true,
6+
"node": true,
7+
"jest": true
8+
},
9+
"plugins": [
10+
"flowtype"
11+
],
12+
"rules": {
13+
"semi": [ 2, "never" ],
14+
"object-curly-newline": [ 0 ],
15+
"object-property-newline": [ 0 ],
16+
"comma-dangle": [ 0 ],
17+
"react/jsx-filename-extension": [2, {
18+
"extensions": [".js", ".jsx"]
19+
}],
20+
"import/no-extraneous-dependencies": [ 0 ],
21+
"react/prop-types": [ 0 ],
22+
"no-confusing-arrow": [ 0 ],
23+
"no-underscore-dangle": [ 0 ],
24+
"no-param-reassign": [ 0 ],
25+
"react/forbid-prop-types": [ 0 ],
26+
"no-plusplus": [ 0 ],
27+
"guard-for-in": [ 0 ],
28+
"no-restricted-syntax": [ 0 ],
29+
"no-continue": [ 1 ],
30+
"no-prototype-builtins": [ 0 ],
31+
"max-len": [ 0, 80 ],
32+
"no-mixed-operators": [ 0 ],
33+
"no-lonely-if": [ 1 ],
34+
"no-bitwise": [ 0 ],
35+
"arrow-parens": [ 0 ],
36+
"function-paren-newline": [ 0 ],
37+
"prefer-destructuring": [ 0 ],
38+
"prefer-template": [ 0 ],
39+
"radix": [ 0 ]
40+
}
41+
}

.gitignore

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# OS or Editor files
2+
._*
3+
.DS_Store
4+
Thumbs.db
5+
6+
# Files that might appear on external disks
7+
.Spotlight-V100
8+
.Trashes
9+
10+
# Always-ignore extensions
11+
*~
12+
*.diff
13+
*.err
14+
*.log
15+
*.orig
16+
*.pyc
17+
*.rej
18+
*.sass-cache
19+
*.sw?
20+
*.vi
21+
22+
node_modules
23+
es
24+
coverage
25+
lib

.prettierrc

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
trailingComma: "es5",
3+
singleQuote: true,
4+
bracketSpacing: true,
5+
jsxBracketSameLine: true,
6+
parser: "babylon",
7+
printWidth: 80,
8+
tabWidth: 2,
9+
useTabs: false,
10+
semi: false
11+
}

.travis.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
language: node_js
2+
node_js:
3+
- "6"
4+
script:
5+
- npm run check
6+
addons:
7+
code_climate:
8+
repo_token: 7b36993e94e8f045924171a92cb833c7097d5a49c98e1905a13964792c25192e
9+
before_script:
10+
- npm run setup
11+
after_script:
12+
- codeclimate-test-reporter < coverage/lcov.info
13+
notifications:
14+
email: false

.vscode/settings.json

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"search.exclude": {
3+
"# OS or Editor files": true,
4+
"._*": true,
5+
".DS_Store": true,
6+
"Thumbs.db": true,
7+
"# Files that might appear on external disks": true,
8+
".Spotlight-V100": true,
9+
".Trashes": true,
10+
"# Always-ignore extensions": true,
11+
"*~": true,
12+
"*.diff": true,
13+
"*.err": true,
14+
"*.log": true,
15+
"*.orig": true,
16+
"*.pyc": true,
17+
"*.rej": true,
18+
"*.sass-cache": true,
19+
"*.sw?": true,
20+
"*.vi": true,
21+
"node_modules": true,
22+
"es": true,
23+
"coverage": true,
24+
"lib": true
25+
},
26+
"files.exclude": {
27+
"# OS or Editor files": true,
28+
"._*": true,
29+
".DS_Store": true,
30+
"Thumbs.db": true,
31+
"# Files that might appear on external disks": true,
32+
".Spotlight-V100": true,
33+
".Trashes": true,
34+
"# Always-ignore extensions": true,
35+
"*~": true,
36+
"*.diff": true,
37+
"*.err": true,
38+
"*.log": true,
39+
"*.orig": true,
40+
"*.pyc": true,
41+
"*.rej": true,
42+
"*.sass-cache": true,
43+
"*.sw?": true,
44+
"*.vi": true,
45+
"node_modules": true,
46+
"es": true,
47+
"coverage": true,
48+
"lib": true
49+
}
50+
}

README.md

+106-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,106 @@
1-
# react-css-component
2-
Inject CSS with a Component
1+
# CSS as a Component
2+
3+
A single React component to inject CSS with ease.<br>
4+
It works with SSR (even using React 16's [renderToNodeStream](https://reactjs.org/docs/react-dom-server.html#rendertonodestream)) and client-side rendering out-of-the-box.
5+
6+
<img alt="TravisCI" src="https://travis-ci.org/rofrischmann/react-css-component.svg?branch=master"> <a href="https://codeclimate.com/github/rofrischmann/react-css-component/coverage"><img alt="Test Coverage" src="https://codeclimate.com/github/rofrischmann/react-css-component/badges/coverage.svg"></a> <img alt="npm version" src="https://badge.fury.io/js/react-css-component.svg"> <img alt="npm downloads" src="https://img.shields.io/npm/dm/react-css-component.svg"> <img alt="dependencies" src="https://david-dm.org/rofrischmann/react-css-component.svg">
7+
8+
## Support Me
9+
If you're using [Robin Frischmann](https://rofrischmann.de)'s work, please consider supporting his [Open Source Projects](https://github.com/rofrischmann) on [**Patreon**](https://www.patreon.com/rofrischmann).
10+
11+
## Installation
12+
```sh
13+
# yarn
14+
yarn add react-css-component
15+
16+
# npm
17+
npm i --save react-css-component
18+
```
19+
It requires `react` and `prop-types` to be present.
20+
21+
## Why?
22+
This package is the result of a [tweet](https://twitter.com/kentcdodds/status/972268883339108352) by [Kent C. Dodds](https://twitter.com/kentcdodds).<br>
23+
Creating resuable React components in general is pretty easy, but adding styling is not. The simplest way is to just use [inline style](https://reactjs.org/docs/dom-elements.html#style), which works pretty well for many basic components. Yet, it is very limited. Neither do we have have pseudo classes nor do we have media queries.<br>
24+
So, at some point, we need actual CSS. We could include a CSS file, but that would require an additional build step e.g. using [Webpack](https://webpack.js.org) in combination with its [css-loader](https://github.com/webpack-contrib/css-loader). While this would work, it's not very compelling to enforce a certain build tool, just to use a single component.<br>
25+
Alright, how about [CSS in JS](http://michelebertoli.github.io/css-in-js/)? Using plain JavaScript to style the component sounds great, as the component is written in JavaScript anyways. But, [most CSS in JS solutions are way to big to depend on](https://github.com/hellofresh/css-in-js-perf-tests#bundle-sizes). They're built for applications, not for independent reusable components. Yet, we can still benefit from writing our CSS in JavaScript. This package is the attempt to provide the smallest universal solution for inlining CSS in JavaScript possible.
26+
27+
## How?
28+
Depending on whether [universal rendering](#universalrendering) (server-side rendering with client-side rehydration) or [client-side only rendering](#clientrendering) is used, the implementation uses a different logic.
29+
30+
### Universal Rendering
31+
**Server Rendering**: The [UniversalStyle](#style) component renders a primitive *style* DOM element that uses `dangerouslySetInnerHTML` to inject a CSS string.
32+
**Client Rehydration**: At first, the [UniversalStyle](#style) component just gets rehydrated to match the server-side markup. But, as soon as it is about to unmount, the *style* element is copied to the `document.head` **once**. This will ensure it's existence just in case any other component using it's CSS is still visible.
33+
34+
##### Caveat
35+
During server-side rendering, the [UniversalStyle](#style) component is not able to track it's own occurence, which may result in duplicate *style* elements. **This won't break anything, but also is not optimal**. To ensure that each [UniversalStyle](#style) instance is only rendered once, we need an unique cache on every render. This is achieved by passing a simple cache via React's context feature. Check the [StyleCacheProvider](#stylecacheprovider) component for more information.
36+
37+
38+
39+
### Client-Only Rendering
40+
If we only render on the client-side anyways, we can skip that complex flow and just use the [ClientStyle](#style).<br>
41+
It simply injects a *style* element into the `document.head` on instantiation and tracks its occurence using a global cache.
42+
43+
## Style
44+
Both `UniversalStyle` and `ClientStyle` share the exact same component API.<br>
45+
This component is the core component and is used to inject the CSS.
46+
47+
### Props
48+
49+
| Parameter | Type | Description |
50+
| --- | --- | --- |
51+
| css | (*string*) | A CSS string that is rendered |
52+
53+
### Import
54+
```javascript
55+
// universal rendering
56+
import { UniversalStyle as Style } from 'react-css-component'
57+
58+
// client-only rendering
59+
importClientStyle as Style } from 'react-css-component'
60+
```
61+
62+
### Example
63+
```javascript
64+
const css = `
65+
.button {
66+
background-color: darkblue;
67+
padding: 20px 10px;
68+
font-size: 16px;
69+
color: white
70+
}
71+
72+
.button:hover {
73+
background-color: blue
74+
}
75+
`
76+
77+
// return an array to colocate style and the button
78+
const Button = () => [
79+
<Style css={css} />
80+
<button className="button">Click Me!</button>
81+
]
82+
```
83+
84+
## StyleCacheProvider
85+
The StyleCacheProvider is used to prevent duplication. It is not required, but recommended.<br>
86+
It should wrap your whole application and is only required once, no matter how many components use the [Style](#style) component.<br>
87+
It does not alter the resulting DOM structure as it simply renders its children.<br>
88+
It passes an empty cache object via React's context feature.
89+
90+
> **Tip**: If you're using the [UniversalStyle](#style) component for a reusable shared component, make sure to inform your users about the caveat of not using this component.
91+
92+
### Example
93+
```javascript
94+
import { StyleCacheProvider } from 'react-css-component'
95+
96+
const App = () => (
97+
<StyleCacheProvider>
98+
<Button />
99+
</StyleCacheProvider>
100+
)
101+
```
102+
103+
## License
104+
react-css-component is licensed under the [MIT License](http://opensource.org/licenses/MIT).<br>
105+
Documentation is licensed under [Creative Common License](http://creativecommons.org/licenses/by/4.0/).<br>
106+
Created with ♥ by [@rofrischmann](http://rofrischmann.de).

package.json

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "react-css-component",
3+
"version": "1.0.0",
4+
"description": "Inject CSS with a Component",
5+
"main": "lib/index.js",
6+
"module": "es/index.js",
7+
"jsnext:main": "es/index.js",
8+
"files": [
9+
"LICENSE",
10+
"README.md",
11+
"lib/**",
12+
"es/**"
13+
],
14+
"keywords": [
15+
"react",
16+
"css",
17+
"css-in-js",
18+
"scoped-css",
19+
"style",
20+
"styling",
21+
"scoped-style"
22+
],
23+
"scripts": {
24+
"build": "babel src -d es --ignore __tests__ && BABEL_ENV=commonjs babel src -d lib --ignore __tests__",
25+
"clean": "rimraf es lib coverage",
26+
"check": "yarn format && yarn lint",
27+
"format": "prettier --write \"src/**/*.js\"",
28+
"lint": "eslint src/**/*.js",
29+
"release": "git pull --rebase && yarn setup && yarn run check && npm publish",
30+
"test": "cross-env BABEL_ENV=commonjs jest",
31+
"test:coverage": "cross-env BABEL_ENV=commonjs jest --coverage",
32+
"watch": "yarn test -- --watch",
33+
"setup": "yarn run clean && yarn build"
34+
},
35+
"repository": "https://github.com/rofrischmann/react-css-component.git",
36+
"author": "Robin Frischmann <[email protected]>",
37+
"license": "MIT",
38+
"devDependencies": {
39+
"babel-cli": "^6.26.0",
40+
"babel-core": "^6.26.0",
41+
"babel-eslint": "^8.2.2",
42+
"babel-jest": "^22.4.1",
43+
"babel-preset-flow": "^6.23.0",
44+
"babel-preset-react": "^6.24.1",
45+
"babel-preset-stage-0": "^6.24.1",
46+
"codeclimate-test-reporter": "^0.3.1",
47+
"cross-env": "^5.1.3",
48+
"eslint": "^4.18.1",
49+
"eslint-config-airbnb": "^16.1.0",
50+
"eslint-plugin-flowtype": "^2.46.1",
51+
"eslint-plugin-import": "^2.9.0",
52+
"eslint-plugin-jsx-a11y": "^6.0.3",
53+
"eslint-plugin-react": "^7.7.0",
54+
"jest": "^22.4.2",
55+
"prettier": "^1.11.1",
56+
"prop-types": "^15.6.1",
57+
"react": "^16.2.0",
58+
"rimraf": "^2.6.2"
59+
},
60+
"peerDependencies": {
61+
"prop-types": "*",
62+
"react": "^16.0.0"
63+
}
64+
}

src/components/ClientStyle.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Component } from 'react'
2+
3+
import { ID_NAMESPACE } from '../utils/namespace'
4+
import appendStyle from '../utils/appendStyle'
5+
import isDOMReady from '../utils/isDOMReady'
6+
7+
const idCache = {}
8+
9+
export default class ClientStyle extends Component {
10+
constructor(props, context) {
11+
super(props, context)
12+
13+
if (!idCache[props.css]) {
14+
// generating a unique style id to prevent duplicate nodes
15+
// within client-sides document.head
16+
const uniqueId = Object.keys(idCache).length
17+
idCache[props.css] = ID_NAMESPACE + uniqueId
18+
}
19+
20+
if (isDOMReady()) {
21+
appendStyle(idCache[props.css], props.css)
22+
this.isReady = true
23+
}
24+
}
25+
26+
componentDidMount() {
27+
if (!this.isReady && isDOMReady()) {
28+
appendStyle(idCache[this.props.css], this.props.css)
29+
}
30+
}
31+
32+
render() {
33+
return null
34+
}
35+
}

0 commit comments

Comments
 (0)