Comparing version 0.1.3 to 1.0.0
122
index.js
@@ -1,102 +0,46 @@ | ||
// Keep track of which styles have been added | ||
// (keyed by hash of a rule set or file path) | ||
import stylis from './stylis.js' | ||
const cache = {} | ||
const hash = () => | ||
Math.random() | ||
.toString(36) | ||
.replace('0.', '') | ||
// The global stylesheet that rules get added to | ||
const sheet = document.createElement('style') | ||
document.head.appendChild(sheet) | ||
export default (strings, ...values) => { | ||
const none = hash => `.${hash}{display:none}` | ||
const hide = hash => (sheet.innerHTML = none(hash) + sheet.innerHTML) | ||
const show = hash => (sheet.innerHTML = sheet.innerHTML.replace(none(hash), '')) | ||
// --------------------------------------- | ||
// A file path was provided | ||
// --------------------------------------- | ||
if (strings[0].startsWith('/')) { | ||
// Use the file name as the uid | ||
const className = 'csz-' + hash(strings[0]) | ||
if(!cache[className]) { | ||
fetch(strings[0]).then(res => res.text()).then(str => { | ||
cache[className] = true | ||
// Prefix every rule in the file with the uid and append style | ||
sheet.innerHTML += str.replace(/(^[^@\s}]*\s*{)/gm, `\n.${className} $1`) | ||
}) | ||
} | ||
return className | ||
} | ||
// --------------------------------------- | ||
// A rule set was supplied | ||
// --------------------------------------- | ||
// Zip constant string parts with any interpolated dynamic values | ||
const str = strings.reduce((acc, string, i) => acc += string + (values[i] == null ? '' : values[i]), '') | ||
// Use a hash of the ruleset as the uid | ||
const className = 'csz-' + hash(str) | ||
if(!cache[className]) { | ||
cache[className] = true | ||
// Prefix the rule set with the uid and append style | ||
sheet.innerHTML += `\n.${className} { ${str} }` | ||
} | ||
return className | ||
const process = key => hash => rules => { | ||
if (key.startsWith('/')) show(hash) | ||
sheet.innerHTML += (cache[key] = { | ||
hash, | ||
rules: stylis()(`.${hash}`, rules), | ||
}).rules | ||
} | ||
// --------------------------------------- | ||
// Hashing functions | ||
// --------------------------------------- | ||
export default (strings, ...values) => { | ||
const key = strings[0].startsWith('/') | ||
? strings[0] | ||
: strings.reduce( | ||
(acc, string, i) => | ||
(acc += string + (values[i] == null ? '' : values[i])), | ||
'' | ||
) | ||
function pad (hash, len) { | ||
while (hash.length < len) { | ||
hash = '0' + hash; | ||
} | ||
return hash; | ||
} | ||
if (cache[key]) return cache[key].hash | ||
function fold (hash, text) { | ||
var i; | ||
var chr; | ||
var len; | ||
if (text.length === 0) { | ||
return hash; | ||
} | ||
for (i = 0, len = text.length; i < len; i++) { | ||
chr = text.charCodeAt(i); | ||
hash = ((hash << 5) - hash) + chr; | ||
hash |= 0; | ||
} | ||
return hash < 0 ? hash * -2 : hash; | ||
} | ||
const className = 'csz-' + hash() | ||
const append = process(key)(className) | ||
function foldObject (hash, o, seen) { | ||
return Object.keys(o).sort().reduce(foldKey, hash); | ||
function foldKey (hash, key) { | ||
return foldValue(hash, o[key], key, seen); | ||
} | ||
} | ||
if (key.startsWith('/')) { | ||
hide(className) | ||
fetch(key) | ||
.then(res => res.text()) | ||
.then(append) | ||
} else append(key) | ||
function foldValue (input, value, key, seen) { | ||
var hash = fold(fold(fold(input, key), toString(value)), typeof value); | ||
if (value === null) { | ||
return fold(hash, 'null'); | ||
} | ||
if (value === undefined) { | ||
return fold(hash, 'undefined'); | ||
} | ||
if (typeof value === 'object') { | ||
if (seen.indexOf(value) !== -1) { | ||
return fold(hash, '[Circular]' + key); | ||
} | ||
seen.push(value); | ||
return foldObject(hash, value, seen); | ||
} | ||
return fold(hash, value.toString()); | ||
return className | ||
} | ||
function toString (o) { | ||
return Object.prototype.toString.call(o); | ||
} | ||
function hash (o) { | ||
return pad(foldValue(0, o, '', []).toString(16), 8); | ||
} |
{ | ||
"name": "csz", | ||
"version": "0.1.3", | ||
"description": "Super lightweight CSS module like behavior at runtime", | ||
"version": "1.0.0", | ||
"description": "Runtime CSS modules with SASS like preprocessing", | ||
"main": "index.js", | ||
@@ -9,2 +9,3 @@ "keywords": [ | ||
"modules", | ||
"preprocessor", | ||
"runtime", | ||
@@ -11,0 +12,0 @@ "tagged template", |
151
README.md
# csz | ||
> Super lightweight CSS module like behavior at runtime | ||
A rudimentary, framework agnostic css-in-js solution that takes styles from a tagged template literal and hashes them to create a unique key which is used as a class name when appending the ruleset to a global stylesheet in the document head at runtime - no build step necessary. It also provides means of importing styles dynamically (an experimental feature) so that you can write your rules in `.css` files as per usual. | ||
> Runtime CSS modules with SASS like preprocessing | ||
A framework agnostic css-in-js solution that uses [stylis](https://github.com/thysultan/stylis.js) to parse styles from tagged template literals; appending processed rulesets (scoped under unique class names) to a global stylesheet in the document head. This all happens at runtime - no build step required. | ||
Importing styles dynamically is also supported (currently an experimental feature), so you can write your rules in `.css` files as per usual. | ||
## Features | ||
- Super light weight and dependency free | ||
- Available as an ES module (imported directly from [unpkg.com](https://unpkg.com/csz)) | ||
- Import styles from a regular `.css` file | ||
- Efficient caching of styles | ||
- Import styles from regular `.css` files | ||
- Available as an ES module (from [unpkg.com](https://unpkg.com/csz)) | ||
- Unique class name generation and namespacing `.csz-lur7p80ssnq` | ||
- Global style injection `:global(selector)` | ||
- Nested selectors `a { &:hover {} }` | ||
- Vendor prefixing `-moz-placeholder` | ||
- Flat stylesheets `color: red; h1 { color: red; }` | ||
- Minification of appended styles | ||
- Keyframe and animation namespacing | ||
## Usage | ||
The package is designed to be used as an ES module. You can import it directly from [unpkg.com](https://unpkg.com): | ||
The package is designed to be used as an ES module. You can import it directly from [unpkg.com](https://unpkg.com/csz/): | ||
```js | ||
import css from 'https://unpkg.com/csz'; | ||
import css from 'https://unpkg.com/csz' | ||
const static = css`background: blue` // generate class name from string | ||
const dynamic = css`/index.css` // generate class name from file | ||
const static = css`background: blue;` // generate class name for ruleset | ||
const dynamic = css`/index.css` // generate class name for file contents | ||
``` | ||
Both variations (static and dynamic) are sync and return a string of the form `csz-b60d61b8`. When using the static method write _naked_ rule sets – no need to wrap the rules in braces or assign an identifier. | ||
Both variations (static and dynamic) are sync and return a string in a format similar to `csz-b60d61b8`. If a ruleset is provided as a string then it is processed immedietly but if a filepath is provided then processing is deffered until the contents of the file has been fetched. | ||
When importing rules from a file then write your rules as you would do normally – with some kind of identifier followed by a rule set wrapped in curly braces. All the rules in the file will be scoped under the hash of the file name. For example: | ||
> All file paths must start with a `/` and be absolute (relative to the current hostname) so if you are running your app on `example.com` and require `/styles/index.css` then csz will try fetch it from `example.com/styles/index.css`. | ||
```css | ||
h1 { background: red; } | ||
.btn { background: blue; } | ||
``` | ||
Styles imported from a file are inevitably going to some amount of time to download. Whilst the stylesheet is being downloaded a temporary ruleset is applied to the element which hides it (using `display: none`) until the fetched files have been processed. This was implemented to prevent flashes of unstyled content. | ||
If your css file looks like the above then it will be appended to the stylesheet like below: | ||
See below for an example of what a raw ruleset might look like and how it looks like after processing. | ||
```css | ||
.csz-b60d61b8 h1 { background: red; } | ||
.csz-b60d61b8 .btn { background: blue; } | ||
``` | ||
<details> | ||
<summary>Example stylesheet (unprocessed)</summary> | ||
```scss | ||
font-size: 2em; | ||
Because the supplied file path is used as the key it will only ever be fetched once. | ||
// line comments | ||
/* block comments */ | ||
:global(body) {background:red} | ||
h1 { | ||
h2 { | ||
h3 { | ||
content:'nesting' | ||
} | ||
} | ||
} | ||
@media (max-width: 600px) { | ||
& {display:none} | ||
} | ||
&:before { | ||
animation: slide 3s ease infinite | ||
} | ||
@keyframes slide { | ||
from { opacity: 0} | ||
to { opacity: 1} | ||
} | ||
& { | ||
display: flex | ||
} | ||
&::placeholder { | ||
color:red | ||
} | ||
``` | ||
</details> | ||
<details> | ||
<summary>Example stylesheet (processed)</summary> | ||
```scss | ||
.csz-a4B7ccH9 {font-size: 2em;} | ||
body {background:red} | ||
h1 h2 h3 {content: 'nesting'} | ||
@media (max-width: 600px) { | ||
.csz-a4B7ccH9 {display:none} | ||
} | ||
.csz-a4B7ccH9:before { | ||
-webkit-animation: slide-id 3s ease infinite; | ||
animation: slide-id 3s ease infinite; | ||
} | ||
@-webkit-keyframes slide-id { | ||
from { opacity: 0} | ||
to { opacity: 1} | ||
} | ||
@keyframes slide-id { | ||
from { opacity: 0} | ||
to { opacity: 1} | ||
} | ||
.csz-a4B7ccH9 { | ||
display:-webkit-box; | ||
display:-webkit-flex; | ||
display:-ms-flexbox; | ||
display:flex; | ||
} | ||
.csz-a4B7ccH9::-webkit-input-placeholder {color:red;} | ||
.csz-a4B7ccH9::-moz-placeholder {color:red;} | ||
.csz-a4B7ccH9:-ms-input-placeholder {color:red;} | ||
.csz-a4B7ccH9::placeholder {color:red;} | ||
``` | ||
</details> | ||
## Example | ||
Here is an example of how you can style a (react) component conditionally based upon some state. | ||
This library is framework agnostic but here is a contrived example of how you can style a React component conditionally based upon some state; demonstrating switching between static and dynamic styles on the fly. | ||
```js | ||
import css from 'https://unpkg.com/csz'; | ||
```jsx | ||
import css from 'https://unpkg.com/csz' | ||
export default () => { | ||
const [toggle, setToggle] = React.useState(false) | ||
return html` | ||
<div className=${toggle ? css`/index.css` : css`background: blue`}> | ||
return ( | ||
<div | ||
className={toggle | ||
? css`/index.css` | ||
: css`background: blue;`} | ||
> | ||
<h1>Hello World!</h1> | ||
<button onClick=${e => setToggle(!toggle)}>Toggle</button> | ||
<button onClick={e => setToggle(!toggle)}>Toggle</button> | ||
</div> | ||
` | ||
) | ||
} | ||
``` | ||
> All file paths must start with a `/` and be absolute (relative to the window location) so if you are running your app on `example.com` and require `/styles/index.css` then csz will try fetch it from `example.com/styles/index.css`. | ||
## Implementation | ||
I was inspired by [emotion](https://github.com/emotion-js/emotion) and [styled-components](https://github.com/styled-components/styled-components) but unfortunately neither of these packages expose an es module compatible build and come with quite a lot of extraneous functionality that isn't required when the scope of the project is restricted to runtime only class name generation with a (very) simple caching mechanism with no cleanup strategy. | ||
There is a lot about this implementation that is not _optimal_. It is a proof of concept more than anything. | ||
I was inspired by [emotion](https://github.com/emotion-js/emotion) and [styled-components](https://github.com/styled-components/styled-components) but unfortunately neither of these packages expose an es module compatible build and come with quite a lot of extraneous functionality that isn't required when the scope of the project is restricted to runtime only class name generation and ruleset isolation. |
@@ -7,7 +7,16 @@ import { React, ReactDOM } from 'https://unpkg.com/es-react' | ||
const App = () => { | ||
const [toggle, setToggle] = React.useState(false) | ||
const [toggle, setToggle] = React.useState(true) | ||
return html` | ||
<div className=${toggle ? css`/test/index.css` : css`background: blue`}> | ||
<div | ||
className=${toggle | ||
? css` | ||
background: blue; | ||
& button { | ||
background: hotpink; | ||
} | ||
` | ||
: css`/test/index.css`} | ||
> | ||
<h1>Hello World!</h1> | ||
<button className='btn' onClick=${e => setToggle(!toggle)}>Toggle</button> | ||
<button className="btn" onClick=${e => setToggle(!toggle)}>Toggle</button> | ||
</div> | ||
@@ -14,0 +23,0 @@ ` |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18995
7
1
150
76