├── .gitignore ├── .jshintrc ├── Makefile ├── README.md ├── examples ├── client-only │ ├── Makefile │ ├── index.html │ ├── index.js │ ├── package.json │ └── styles │ │ ├── blue-button.css │ │ ├── button.css │ │ ├── main.css │ │ └── red-button.css └── server-rendering │ ├── Makefile │ ├── client.js │ ├── server.js │ └── styles │ ├── blue-button.css │ ├── button.css │ ├── main.css │ └── red-button.css ├── index.js ├── lib ├── ReactStylesheetMixin.js ├── StylesheetImage.js └── renderComponentToString.js ├── package.json └── tests ├── ReactStylesheetMixin-browser.js └── ReactStylesheetMixin-server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | examples/client-only/bundle.js 3 | examples/client-only/node_modules/ 4 | examples/server-rendering/client.js 5 | examples/server-rendering/bundle.js 6 | examples/server-rendering/node_modules/ 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": "vars", 4 | "asi": true, 5 | "expr": true, 6 | "globalstrict": true, 7 | "globals": { 8 | "Buffer": false, 9 | "document": false, 10 | "process": false, 11 | "module": false, 12 | "exports": false, 13 | "__filename": false, 14 | "__dirname": false, 15 | "require": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | PATH := $(BIN):$(PATH) 3 | 4 | TEST_SUITES = $(wildcard tests/*.js) 5 | TEST_SUITES_COMMON = $(filter-out %-browser.js %-server.js, $(TEST_SUITES)) 6 | TEST_SUITES_BROWSER = $(filter %-browser.js, $(TEST_SUITES)) 7 | TEST_SUITES_SERVER = $(filter %-server.js, $(TEST_SUITES)) 8 | 9 | install link: 10 | @npm $@ 11 | 12 | lint: 13 | @jshint *.js lib/*.js 14 | 15 | test:: test-server test-browser 16 | 17 | test-server:: 18 | @mocha -R spec $(TEST_SUITES_COMMON) $(TEST_SUITES_SERVER) 19 | 20 | test-browser: 21 | @browserify -d -p [ mocaccino -R spec ] \ 22 | $(TEST_SUITES_COMMON) $(TEST_SUITES_BROWSER) \ 23 | | phantomic 24 | 25 | release-patch: test lint 26 | @$(call release,patch) 27 | 28 | release-minor: test lint 29 | @$(call release,minor) 30 | 31 | release-major: test lint 32 | @$(call release,major) 33 | 34 | publish: 35 | git push --tags origin HEAD:master 36 | npm publish 37 | 38 | define release 39 | npm version $(1) 40 | endef 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-stylesheet 2 | 3 | **Deprecated, see**: https://github.com/prometheusresearch/react-stylesheet 4 | 5 | A mixin for React components to declare stylesheet dependencies. 6 | 7 | ## Motivation 8 | 9 | We want to create React components which are completely self-contained and 10 | include all needed styling. Such components should be distributed using a 11 | package manager (probably npm) and be easy to reuse. 12 | 13 | ## Installation 14 | 15 | % npm install react-stylesheet 16 | 17 | ## Usage 18 | 19 | Library exports a single mixin `ReactStylesheet` which expects a component to 20 | provide `stylesheets` attribute which should be an array of URLs to stylesheets: 21 | 22 | var React = require('react') 23 | var ReactStylesheet = require('react-stylesheet') 24 | 25 | var Button = React.createClass({ 26 | mixins: [ReactStylesheet], 27 | 28 | stylesheets: [ 29 | "/assets/widgets/button.css" 30 | ], 31 | 32 | render: function() { 33 | return {this.props.label} 34 | } 35 | }) 36 | 37 | After rendering the component, the declared stylesheets will be inserted into 38 | document's head. 39 | 40 | The idea is that you should be able to add stylesheets to the document even if 41 | your component doesn't render the `` element. 42 | 43 | But it's obvious that knowing all URLs beforehand when packaging a reusable 44 | component isn't possible. Fortunately, there are tools which address that. 45 | 46 | ## Using with require-assets 47 | 48 | You can use [require-assets][] library to reference static assets from npm 49 | packages. This library exports a single function `requireAssets` which resolve 50 | an asset identifier into an URL. 51 | 52 | var React = require('react'); 53 | var ReactStylesheet = require('react-stylesheet'); 54 | var requireAssets = require('require-assets'); 55 | 56 | var Button = React.createClass({ 57 | stylesheets: [ 58 | requireAssets('./styles.css') 59 | ], 60 | 61 | render: function() { 62 | return this.props.children 63 | } 64 | }) 65 | 66 | [require-assets]: https://github.com/andreypopp/require-assets 67 | 68 | ## Server-side rendering 69 | 70 | If you use fullpage rendering and prerender your UI on server with 71 | `React.renderComponentToString(...)`, then all the `` tags will be 72 | rendered inside the `` tag. 73 | 74 | ## Implementation notes 75 | 76 | **Garbage collection for stylesheets.** It is possible to extend 77 | `react-stylesheet` to allow to purge unused stylesheets. This can be done by 78 | storing a reference counter for each stylesheet. When such a counter hits 0, we 79 | can remove the corresponding stylesheet from the DOM. That could hit some edge 80 | cases where some small UI update can trigger a style recalc and reflow, to avoid 81 | that we can invent more advanced strategies to purge unused stylesheets, e.g. 82 | when number of CSS rules is above the threshold, do some time-ammortized purges 83 | and so on... 84 | 85 | **Stylesheet bundling.** It is easy to extend `react-stylesheet` to support 86 | bundled stylesheets. We just need to remap original CSS reference to a bundle 87 | reference. It doesn't matter if we bundle all stylesheet into one big bundle or 88 | split bundles per UI screens — this mechanism support both scenarious. This 89 | integrates smoothly with the garbage collection mechanism described above. 90 | -------------------------------------------------------------------------------- /examples/client-only/Makefile: -------------------------------------------------------------------------------- 1 | BIN = ../../node_modules/.bin 2 | PATH := $(BIN):$(PATH) 3 | 4 | build: bundle.js 5 | 6 | clean: 7 | @rm -f bundle.js 8 | 9 | bundle.js: index.js ../../index.js ../../lib/*.js 10 | @browserify -d -t reactify index.js > $@ 11 | -------------------------------------------------------------------------------- /examples/client-only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/client-only/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = require('react'); 6 | var ReactStylesheet = require('../../'); 7 | 8 | var App = React.createClass({ 9 | mixins: [ReactStylesheet], 10 | 11 | stylesheets: [ 12 | "styles/main.css" 13 | ], 14 | 15 | getInitialState: function() { 16 | return {coin: true}; 17 | }, 18 | onClick: function(coin) { 19 | this.setState({coin: coin}); 20 | }, 21 | render: function() { 22 | return ( 23 |
24 | Hello! 25 | {this.state.coin ? 26 | : 27 | } 28 |
29 | ); 30 | } 31 | }); 32 | 33 | var RedButton = React.createClass({ 34 | mixins: [ReactStylesheet], 35 | 36 | stylesheets: [ 37 | "styles/button.css", 38 | "styles/red-button.css" 39 | ], 40 | 41 | render: function() { 42 | return ( 43 | 44 | RED 45 | 46 | ); 47 | } 48 | }); 49 | 50 | var BlueButton = React.createClass({ 51 | mixins: [ReactStylesheet], 52 | 53 | stylesheets: [ 54 | "styles/button.css", 55 | "styles/blue-button.css" 56 | ], 57 | 58 | render: function() { 59 | return ( 60 | 61 | BLUE 62 | 63 | ); 64 | } 65 | }); 66 | 67 | window.onload = function() { 68 | React.renderComponent(App(), document.body); 69 | } 70 | -------------------------------------------------------------------------------- /examples/client-only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stylesheet-example-client-only", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "browserify -d -t reactify index.js > bundle.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /examples/client-only/styles/blue-button.css: -------------------------------------------------------------------------------- 1 | .BlueButton { 2 | background: blue; 3 | } 4 | -------------------------------------------------------------------------------- /examples/client-only/styles/button.css: -------------------------------------------------------------------------------- 1 | .Button { 2 | cursor: pointer; 3 | padding: 5px; 4 | border: 1px solid black; 5 | } 6 | -------------------------------------------------------------------------------- /examples/client-only/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #aeaeae; 3 | font-size: 200%; 4 | } 5 | -------------------------------------------------------------------------------- /examples/client-only/styles/red-button.css: -------------------------------------------------------------------------------- 1 | .RedButton { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/server-rendering/Makefile: -------------------------------------------------------------------------------- 1 | BIN = ../../node_modules/.bin 2 | PATH := $(BIN):$(PATH) 3 | 4 | start: build 5 | @node server.js 6 | 7 | build: bundle.js 8 | 9 | clean: 10 | @rm -f bundle.js 11 | 12 | bundle.js: client.js ../../index.js ../../lib/*.js 13 | @browserify -d -t reactify client.js > $@ 14 | -------------------------------------------------------------------------------- /examples/server-rendering/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | var React = require('react'); 6 | var ReactStylesheet = require('../../'); 7 | 8 | var App = React.createClass({ 9 | mixins: [ReactStylesheet], 10 | 11 | stylesheets: [ 12 | "styles/main.css", 13 | ], 14 | 15 | render: function() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | }); 28 | 29 | var Body = React.createClass({ 30 | getInitialState: function() { 31 | return {coin: true}; 32 | }, 33 | 34 | onClick: function(coin) { 35 | this.setState({coin: coin}); 36 | }, 37 | 38 | render: function() { 39 | return ( 40 |
41 |

Hello!

42 | {this.state.coin ? 43 | : 44 | } 45 |
46 | ); 47 | } 48 | }); 49 | 50 | var RedButton = React.createClass({ 51 | mixins: [ReactStylesheet], 52 | 53 | stylesheets: [ 54 | "styles/button.css", 55 | "styles/red-button.css" 56 | ], 57 | 58 | render: function() { 59 | return ( 60 | RED 63 | ); 64 | } 65 | }); 66 | 67 | var BlueButton = React.createClass({ 68 | mixins: [ReactStylesheet], 69 | 70 | stylesheets: [ 71 | "styles/button.css", 72 | "styles/blue-button.css" 73 | ], 74 | 75 | render: function() { 76 | return ( 77 | BLUE 80 | ); 81 | } 82 | }); 83 | 84 | if (typeof window !== 'undefined') { 85 | window.onload = function() { 86 | React.renderComponent(App(), document); 87 | } 88 | } 89 | 90 | module.exports = App; 91 | -------------------------------------------------------------------------------- /examples/server-rendering/server.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var express = require('express'); 3 | var nodejsx = require('node-jsx').install(); 4 | var App = require('./client'); 5 | 6 | express() 7 | .use(express.static(__dirname)) 8 | .get('/', function(req, res, next) { 9 | try { 10 | var markup = React.renderComponentToString(App()); 11 | res.send(markup); 12 | } catch(err) { 13 | next(err); 14 | } 15 | }) 16 | .listen(3000, function() { 17 | console.log('point your browser at http://localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/server-rendering/styles/blue-button.css: -------------------------------------------------------------------------------- 1 | .BlueButton { 2 | background: blue; 3 | } 4 | -------------------------------------------------------------------------------- /examples/server-rendering/styles/button.css: -------------------------------------------------------------------------------- 1 | .Button { 2 | cursor: pointer; 3 | padding: 5px; 4 | border: 1px solid black; 5 | } 6 | -------------------------------------------------------------------------------- /examples/server-rendering/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #aeaeae; 3 | font-size: 200%; 4 | } 5 | -------------------------------------------------------------------------------- /examples/server-rendering/styles/red-button.css: -------------------------------------------------------------------------------- 1 | .RedButton { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var renderComponentToString = require('./lib/renderComponentToString'); 5 | var ReactStylesheetMixin = require('./lib/ReactStylesheetMixin'); 6 | 7 | React.renderComponentToString = renderComponentToString; 8 | 9 | module.exports = ReactStylesheetMixin; 10 | -------------------------------------------------------------------------------- /lib/ReactStylesheetMixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var StylesheetImage = require('./StylesheetImage'); 4 | 5 | /** 6 | * Mixin for React component to define stylesheets they depend on. 7 | */ 8 | var ReactStylesheetMixin = { 9 | 10 | _ensureImagesMounted: function() { 11 | for (var i = 0, len = this.stylesheets.length; i < len; i++) { 12 | var href = this.stylesheets[i]; 13 | if (!StylesheetImage.hasImage(href)) { 14 | StylesheetImage.insertImage(href) 15 | } 16 | } 17 | }, 18 | 19 | componentWillMount: function() { 20 | var root = getRootComponent(this); 21 | var registry = root.__stylesheets = root.__stylesheets || {}; 22 | 23 | for (var i = 0, len = this.stylesheets.length; i < len; i++) { 24 | registry[this.stylesheets[i]] = true; 25 | } 26 | }, 27 | 28 | componentDidMount: function() { 29 | this._ensureImagesMounted(); 30 | }, 31 | 32 | componentDidUpdate: function() { 33 | this._ensureImagesMounted(); 34 | }, 35 | }; 36 | 37 | 38 | /** 39 | * Get root component in the hierarchy 40 | * 41 | * @private 42 | * 43 | * @param {ReactComponent} component 44 | */ 45 | function getRootComponent(component) { 46 | while (component._owner) { 47 | component = component._owner; 48 | } 49 | return component; 50 | } 51 | 52 | module.exports = ReactStylesheetMixin; 53 | -------------------------------------------------------------------------------- /lib/StylesheetImage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ExecutionEnvironment = require('react/lib/ExecutionEnvironment'); 4 | 5 | var activeImages = {}; 6 | 7 | if (ExecutionEnvironment.canUseDOM) { 8 | populateActiveImages(); 9 | } 10 | 11 | function populateActiveImages() { 12 | var images = document.head.querySelectorAll('link[data-react-stylesheet]'); 13 | for (var i = 0, len = images.length; i < len; i++) { 14 | activeImages[images[i].getAttribute('href')] = true; 15 | } 16 | } 17 | 18 | /** 19 | * Create image markup for the stylesheet. 20 | * 21 | * @private 22 | * 23 | * @param {String} href 24 | */ 25 | function createImageMarkup(href) { 26 | return ''; 27 | } 28 | 29 | /** 30 | * Create DOM node image for the stylesheet. 31 | * 32 | * @param {String} href 33 | */ 34 | function createImageNode(href) { 35 | var link = document.createElement('link'); 36 | link.dataset.reactStylesheet = "true"; 37 | link.rel = 'stylesheet'; 38 | link.href = href; 39 | return link; 40 | } 41 | 42 | /** 43 | * Get DOM node image for the stylesheet or null if there are no corresponding 44 | * images exists in DOM. 45 | * 46 | * @param {String} href 47 | */ 48 | function hasImage(href) { 49 | return activeImages[href]; 50 | } 51 | 52 | /** 53 | * Insert stylesheet image into DOM. 54 | * 55 | * @param {String} href 56 | */ 57 | function insertImage(href) { 58 | var node = createImageNode(href); 59 | var images = document.head.querySelectorAll('link[data-react-stylesheet]'); 60 | 61 | if (images.length === 0) { 62 | if (document.head.firstChild) { 63 | document.head.insertBefore(node, document.head.firstChild); 64 | } else { 65 | document.head.appendChild(node); 66 | } 67 | } else { 68 | var last = images[images.length - 1]; 69 | if (last.nextSibling) { 70 | document.head.insertBefore(node, last.nextSibling); 71 | } else { 72 | document.head.appendChild(node); 73 | } 74 | } 75 | 76 | activeImages[href] = true; 77 | } 78 | 79 | module.exports = { 80 | createImageMarkup: createImageMarkup, 81 | createImageNode: createImageNode, 82 | insertImage: insertImage, 83 | hasImage: hasImage 84 | }; 85 | -------------------------------------------------------------------------------- /lib/renderComponentToString.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react'); 4 | var StylesheetImage = require('./StylesheetImage'); 5 | 6 | var renderComponentToStringImpl = React.renderComponentToString; 7 | 8 | /** 9 | * Render component to string 10 | * 11 | * This mimics the original React.renderComponentToString but injects collected 12 | * stylesheets into markup's . 13 | * 14 | * @param {Component} component 15 | */ 16 | function renderComponentToString(component) { 17 | var markup = renderComponentToStringImpl(component); 18 | if (component.__stylesheets) { 19 | markup = injectStylesheetImages(markup, component.__stylesheets); 20 | } 21 | return markup; 22 | 23 | } 24 | 25 | /** 26 | * Inject stylesheet images into markup. 27 | * 28 | * @private 29 | * 30 | * @param {String} markup 31 | * @param {Object} stylesheets 32 | */ 33 | function injectStylesheetImages(markup, stylesheets) { 34 | var injection = Object.keys(stylesheets) 35 | .map(StylesheetImage.createImageMarkup) 36 | .join(''); 37 | if (markup.match(/<\/head>/)) { 38 | markup = markup.replace(/]*>/, "$&" + injection); 39 | } else { 40 | markup = injection + markup; 41 | } 42 | return markup; 43 | } 44 | 45 | module.exports = renderComponentToString; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stylesheet", 3 | "version": "0.1.1", 4 | "description": "A component for React to declare stylesheet dependencies", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "react": "~0.9.0", 8 | "node-jsx": "~0.9.0", 9 | "reactify": "~0.9.0", 10 | "express": "~3.4.8", 11 | "browserify": "^3.31.2", 12 | "jshint": "^2.4.4", 13 | "mocaccino": "^0.3.1", 14 | "phantomic": "^0.4.2", 15 | "mocha": "^1.17.1", 16 | "phantomjs": "^1.9.7-1" 17 | }, 18 | "peerDependencies": { 19 | "react": "~0.9.0" 20 | }, 21 | "scripts": { 22 | "test": "make test" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/andreypopp/react-stylesheet" 27 | }, 28 | "keywords": [ 29 | "react-component", 30 | "stylesheet" 31 | ], 32 | "author": "Andrey Popp <8mayday@gmail.com>", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/andreypopp/react-stylesheet/issues" 36 | }, 37 | "homepage": "https://github.com/andreypopp/react-stylesheet" 38 | } 39 | -------------------------------------------------------------------------------- /tests/ReactStylesheetMixin-browser.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var React = require('react'); 3 | var ReactTestUtils = require('react/lib/ReactTestUtils'); 4 | var ReactStylesheetMixin = require('../lib/ReactStylesheetMixin'); 5 | 6 | describe('ReactStylesheetMixin (browser)', function() { 7 | 8 | var Component = React.createClass({ 9 | mixins: [ReactStylesheetMixin], 10 | 11 | stylesheets: [ 12 | '/assets/style.css' 13 | ], 14 | 15 | render: function() { 16 | return React.DOM.div(null, 'Hello'); 17 | } 18 | }); 19 | 20 | it('inserts stylesheet into DOM on mount', function() { 21 | ReactTestUtils.renderIntoDocument(Component()); 22 | var links = document.head.querySelectorAll('link'); 23 | assert.equal(links.length, 1); 24 | var link = links[0]; 25 | assert(link.dataset.reactStylesheet); 26 | assert(link.href.match(/\/assets\/style\.css$/)); 27 | }); 28 | 29 | it('does not duplicate link elements in DOM', function() { 30 | ReactTestUtils.renderIntoDocument(Component()); 31 | ReactTestUtils.renderIntoDocument(Component()); 32 | var links = document.head.querySelectorAll('link'); 33 | assert.equal(links.length, 1); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /tests/ReactStylesheetMixin-server.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var React = require('react'); 3 | var ReactStylesheetMixin = require('../lib/ReactStylesheetMixin'); 4 | var renderComponentToString = require('../lib/renderComponentToString'); 5 | 6 | describe('ReactStylesheetMixin (server)', function() { 7 | 8 | var Component = React.createClass({ 9 | mixins: [ReactStylesheetMixin], 10 | 11 | stylesheets: [ 12 | '/assets/style.css' 13 | ], 14 | 15 | render: function() { 16 | return React.DOM.div(null, 'Hello'); 17 | } 18 | }); 19 | 20 | it('renders into a markup which contains elements', function() { 21 | var markup = renderComponentToString(Component()); 22 | assert(markup.match(/^ elements in (if it presents)', function() { 36 | var markup = renderComponentToString(App()); 37 | assert(markup.match(/]*>