├── .gitignore ├── Cress.js ├── Cress.min.js ├── README.md ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /Cress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constructor for creating Cress stylesheet components 3 | * @param {String} selector The selector to match nodes that will become instances of the Cress component 4 | * @param {Object} config The configuration object. See [the README on Github]{@link https://github.com/Heydon/cress/blob/master/README.md} 5 | * @class 6 | */ 7 | function Cress(selector, config) { 8 | this.elems = Array.from(document.querySelectorAll(selector)); 9 | this.this = config.this || 'this'; 10 | this.pattern = new RegExp(this.this, 'g'); 11 | this.sharedId = Math.random().toString(36).substr(2, 9); 12 | this.reset = `${this.this}, ${this.this} * { all: initial }`; 13 | 14 | this.getProps = elem => { 15 | if (!config.props) { 16 | return null; 17 | } 18 | let props = {}; 19 | for (let prop in config.props) { 20 | let attr = elem.getAttribute(`data-cress-${prop}`); 21 | props[prop] = attr ? attr : config.props[prop]; 22 | } 23 | return props; 24 | } 25 | 26 | this.styleUp = elem => { 27 | let props = this.getProps(elem); 28 | let i = !props ? this.sharedId : this.sharedId + '-' + Object.values(props).join(''); 29 | elem.dataset.cress = i; 30 | 31 | let styles = config.reset ? this.reset + config.css(props) : config.css(props); 32 | let css = styles.replace(this.pattern, `[data-cress="${i}"]`); 33 | 34 | if (!document.getElementById(i)) { 35 | document.head.innerHTML += ` 36 | 37 | `; 38 | } 39 | } 40 | 41 | this.elems.forEach(elem => { 42 | config.props && new MutationObserver(() => { 43 | this.styleUp(elem); 44 | }).observe(elem, { 45 | attributes: true, 46 | attributeFilter: Object.keys(config.props).map(k => `data-${k}`) 47 | }); 48 | 49 | if ('ResizeObserver' in window && config.resize) { 50 | new ResizeObserver(entries => { 51 | config.resize(entries[0]); 52 | }).observe(elem); 53 | } 54 | 55 | this.styleUp(elem); 56 | }); 57 | } 58 | 59 | export { Cress }; -------------------------------------------------------------------------------- /Cress.min.js: -------------------------------------------------------------------------------- 1 | function Cress(e,t){this.elems=Array.from(document.querySelectorAll(e)),this.this=t.this||"this",this.pattern=new RegExp(this.this,"g"),this.sharedId=Math.random().toString(36).substr(2,9),this.reset=`${this.this}, ${this.this} * { all: initial }`,this.getProps=(e=>{if(!t.props)return null;let s={};for(let r in t.props){let i=e.getAttribute(`data-cress-${r}`);s[r]=i||t.props[r]}return s}),this.styleUp=(e=>{let s=this.getProps(e),r=s?this.sharedId+"-"+Object.values(s).join(""):this.sharedId;e.dataset.cress=r;let i=(t.reset?this.reset+t.css(s):t.css(s)).replace(this.pattern,`[data-cress="${r}"]`);document.getElementById(r)||(document.head.innerHTML+=`\n \n `)}),this.elems.forEach(e=>{t.props&&new MutationObserver(()=>{this.styleUp(e)}).observe(e,{attributes:!0,attributeFilter:Object.keys(t.props).map(e=>`data-${e}`)}),"ResizeObserver"in window&&t.resize&&new ResizeObserver(e=>{t.resize(e[0])}).observe(e),this.styleUp(e)})}export{Cress}; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cress 2 | 3 | **Cress** stands for **Componentized Reactive ...Erm... Scoped Stylesheets**. Think of **Cress** components as sort of like web components just for styling, except without having to use the web components specs. 4 | 5 | **Cress** is intended as a smaller, more versatile, and less complex alternative to other CSS-in-JS solutions. It encourages separating styling from markup and behavior, while retaining a component-like organizational philosophy. 6 | 7 | Where SSR (Server-side Rendering) is applied, the basic componentized/scoped styling behaviour it offers works in IE7+. 8 | 9 | **WARNING: This is very experimental, and in the early stages of development** 10 | 11 | * [Features](#features) 12 | * [The `this` keyword... in CSS??](#the-this-keyword-in-CSS) 13 | * [Props](#props) 14 | * [Reactivization](#reactivization) 15 | * [Container queries too??](#container-queries-too) 16 | * [SSR](#ssr) 17 | * [What about Shadow DOM? Isn't that what everyone wants?](#what-about-shadow-dom-isnt-that-what-everyone-wants) 18 | * [Config API](#config-api) 19 | 20 | ## Features 21 | 22 | * Completely dependency free 23 | * Completely framework independent 24 | * 1KB minified ES module 25 | * Automatic scoping 26 | * SSR (Server-side Rendering) is easy 27 | * Reactive to prop changes using `MutationObserver` 28 | * `resize` method harnessing `ResizeObserver` 29 | 30 | ## The `this` keyword... in CSS?? 31 | 32 | Well, no, not really. The word ”this” in a string that looks like CSS is more like it. But it helps solve the scoping issue without having to worry about the CSS OM or having to use some sort of complex regular expression. Instead, you just write ”this” (or a different word you've chosen; it's configurable) wherever you need to represent the target/parent element in the selector. 33 | 34 | ```css 35 | this button { 36 | color: red; 37 | } 38 | ``` 39 | 40 | In the constructor, ”this” is replaced globally by a special identifier, and that's how styles are scoped: 41 | 42 | ```css 43 | [data-cress="wi5xdkbb9"] button { 44 | color: red; 45 | } 46 | ``` 47 | 48 | ## Props 49 | 50 | When you create a new **Cress** component, you can set some default props and interpolate these into the `css` method: 51 | 52 | ```js 53 | new Cress('.test', { 54 | props: { 55 | color: 'red' 56 | }, 57 | css(props) { 58 | return ` 59 | this button { 60 | color: ${props.color}; 61 | } 62 | ` 63 | } 64 | }); 65 | ``` 66 | 67 | The neat thing is that every element matching `.test` will now share an embedded stylesheet identified by `wi5xdkbb9-red`. The `wi5xdkbb9` part is shared by _all_ `.test` elements, and the `red` part is shared by all `.test` elements _where the `color` prop is `red`_. This saves on redundancy. 68 | 69 | Now... if you apply the `data-cress-color="blue"` attribution to one or more of your `.test` elements, their identifier will become `wi5xdkbb9-blue` and a new stylesheet will be created to serve them differently. 70 | 71 | Every default prop can be overridden with an attribute of the pattern `data-cress-[name of prop]`. You don't need to use props (you may just want the scoping feature) but they're v nice. 72 | 73 | ## Reactivization 74 | 75 | A `MutationObserver` is applied to each of the `.test` elements, so when a prop-specific attribute is changed the new styles are applied. That's the reactive part, and makes **Cress** components a bit like custom elements. However, they are much easier to SSR: custom elements are [not currently supported by JSDOM, for instance](https://github.com/jsdom/jsdom/issues/1030). 76 | 77 | `MutationObserver` is [nearly universally supported](https://caniuse.com/#feat=mutationobserver) in browsers; [Custom Elements are not nearly universally supported](https://caniuse.com/#feat=mutationobserver&search=custom%20elements). 78 | 79 | ## Container queries too?? 80 | 81 | **Cress** components are initialized with two arguments: a CSS selector, and a config object. As well as the default `props` object and the `css()` method, you can add and adjust some other things too. The `resize()` method lets you do stuff when a (parent) element (the element that matches the selector) is resized. This allows you to create container queries! 82 | 83 | Consider the following example: 84 | 85 | ```js 86 | new Cress('.test', { 87 | props: { 88 | fontBig: '1.5rem', 89 | fontSmall: '0.85rem' 90 | }, 91 | css(props) { 92 | return ` 93 | this button { 94 | font-size: ${props.fontBig}; 95 | } 96 | 97 | this.small button { 98 | font-size: ${props.fontSmall}; 99 | } 100 | ` 101 | }, 102 | resize(observed) { 103 | observed.target.classList.toggle( 104 | 'small', 105 | observed.contentRect.width < 400 106 | ); 107 | } 108 | }); 109 | ``` 110 | 111 | Wherever `.test` elements are narrower than `400px`, they will adopt the `.small` class and the `props.fontSmall` text size. Goodness! (Note that `observed` represents the `ResizeObserver` entry for the `.test` node, and `observed.target` represents the node itself.) 112 | 113 | `ResizeObserver` is [not the most well supported observer](https://caniuse.com/#search=resizeObserver) (although it has recently come to Firefox), so it's recommended `resize()` stuff is limited to progressive enhancements only, for production sites, for now. 114 | 115 | ## SSR 116 | 117 | Fundamentally, **Cress** just embeds some [automatically scoped](#the-this-keyword-in-CSS) CSS. It doesn't need any kind of JavaScript to run in the browser if SSR (Server-side Rendering) has already populated the `` with those styles. Why should basic CSS depend on JavaScript?? 118 | 119 | Your SSR approach is up to you. I like JSDOM; you might use Chromium and Puppeteer. In any case, all that needs to happen is for the **Cress** constructors to run and the HTML string to be augmented at build time. With the static site generator Eleventy, I can use a `transform` function that post-processes the HTML. The `runScripts` option is the key; it's what runs the **Cress** code and embeds the styles. 120 | 121 | ```js 122 | eleventyConfig.addTransform('ssr', function(page) { 123 | let dom = new JSDOM(page, { 124 | resources: 'usable', 125 | runScripts: 'dangerously' 126 | }); 127 | let document = dom.window.document; 128 | return '\r\n' + document.documentElement.outerHTML; 129 | }); 130 | ``` 131 | 132 | Yes, **Cress** components are [reactive](#reactivization) and can incorporate JavaScript-enabled [container queries](#container-queries-too), but these features should be considered progressive enhancements. Server-side rendered **Cress** components rely on nothing more than `