├── .eslintignore ├── .travis.yml ├── .gitignore ├── .npmignore ├── css.js ├── component.js ├── test_flow ├── package.json ├── .flowconfig └── index.js ├── src ├── getComponentDisplayName.js ├── __tests__ │ ├── setup.js │ ├── index-test.js │ ├── StyleableDOMComponent-test.js │ └── DOMStylesheet-test.js ├── index.js.flow ├── StyleableDOMComponent.js ├── index.js ├── CSS.js ├── component.js └── DOMStylesheet.js ├── .flowconfig ├── LICENSE ├── Makefile ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/__tests__/*.js 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - stable 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/bundle.js 3 | lib/ 4 | coverage/ 5 | .nyc_output/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | npm-debug.log 3 | coverage/ 4 | .nyc_output/ 5 | test_flow/ 6 | Makefile 7 | -------------------------------------------------------------------------------- /css.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Prometheus Research, LLC 3 | * @flow 4 | */ 5 | 'use strict'; 6 | 7 | module.exports = require('./lib/CSS'); 8 | -------------------------------------------------------------------------------- /component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016-present, Prometheus Research, LLC 3 | */ 4 | 'use strict'; 5 | 6 | module.exports = require('./lib/component'); 7 | 8 | -------------------------------------------------------------------------------- /test_flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dom-stylesheet-test-flow", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "react": "^15.2.1", 6 | "react-dom-stylesheet": "../" 7 | }, 8 | "devDependencies": { 9 | "flow-bin": "^0.31.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/getComponentDisplayName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Prometheus Research, LLC 3 | */ 4 | 5 | export default function getComponentDisplayName(Component) { 6 | return typeof Component === 'string' ? 7 | Component : 8 | Component.displayName || Component.name || 'Component'; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test_flow/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/fbjs/.*.flow 3 | /node_modules/fbjs/.*.js 4 | /node_modules/.store/fbjs@.*/.*.flow 5 | /node_modules/.store/fbjs@.*/.*.js 6 | 7 | [include] 8 | 9 | [libs] 10 | 11 | [options] 12 | suppress_comment=\\(.\\|\\n\\)*\\$ExpectError 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/fbjs/.*.flow 3 | /node_modules/fbjs/.*.js 4 | /node_modules/.store/fbjs@.*/.*.flow 5 | /node_modules/.store/fbjs@.*/.*.js 6 | /test_flow/node_modules/fbjs/.*.flow 7 | /test_flow/node_modules/fbjs/.*.js 8 | /test_flow/node_modules/.store/fbjs@.*/.*.flow 9 | /test_flow/node_modules/.store/fbjs@.*/.*.js 10 | 11 | [include] 12 | 13 | [libs] 14 | 15 | [options] 16 | suppress_comment=\\(.\\|\\n\\)*\\$ExpectError 17 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Prometheus Research, LLC 3 | */ 4 | 5 | import assert from 'power-assert'; 6 | import jsdom from 'jsdom'; 7 | 8 | let document = jsdom.jsdom(''); 9 | let window = document.defaultView; 10 | 11 | global.assert = assert; 12 | global.document = document; 13 | global.window = window; 14 | 15 | propagateToGlobal(window); 16 | 17 | function propagateToGlobal(window) { 18 | for (let key in window) { 19 | if (!window.hasOwnProperty(key)) { 20 | continue; 21 | } 22 | if (key in global) { 23 | continue; 24 | } 25 | global[key] = window[key]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016+ Prometheus Research, LLC 3 | * @flow 4 | */ 5 | 6 | declare export function style( 7 | Component: T, 8 | stylesheetSpec: Object, 9 | options?: string | {displayName?: string} 10 | ): T; 11 | 12 | declare export function wrapWithStylesheet( 13 | Component: T, 14 | stylesheet: Object, 15 | options: string | {displayName?: string} 16 | ): T; 17 | 18 | export type DOMStylesheet = { 19 | use(): void; 20 | dispose(): void; 21 | asClassName(variant?: Object): string; 22 | override(stylesheetSpec: Object | DOMStylesheet): DOMStylesheet; 23 | }; 24 | 25 | declare export function create(stylesheetSpec: Object): DOMStylesheet; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Prometheus Research 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test_flow/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import {style} from 'react-dom-stylesheet'; 6 | import React from 'react'; 7 | 8 | function Functional({hello}: {hello: string}) { 9 | return
{hello}
; 10 | } 11 | 12 | ; 13 | 14 | // $ExpectError 15 | ; 16 | 17 | let StyledFunctional = style(Functional, { 18 | color: 'red', 19 | }); 20 | 21 | ; 22 | 23 | // $ExpectError 24 | ; 25 | 26 | class Class extends React.Component { 27 | 28 | props: { 29 | hello: string; 30 | }; 31 | 32 | render() { 33 | return
{this.props.hello}
; 34 | } 35 | } 36 | 37 | ; 38 | 39 | // $ExpectError 40 | ; 41 | 42 | let StyledClass = style(Class, { 43 | color: 'red', 44 | }); 45 | 46 | ; 47 | 48 | // $ExpectError 49 | ; 50 | 51 | import {create} from 'react-dom-stylesheet'; 52 | 53 | let stylesheet = create({ 54 | color: 'red' 55 | }); 56 | 57 | stylesheet.use(); 58 | 59 | stylesheet.dispose(); 60 | 61 | let className = stylesheet.asClassName({ok: true}); 62 | 63 | let nextStylesheet = stylesheet.override({ 64 | background: 'red', 65 | }); 66 | -------------------------------------------------------------------------------- /src/StyleableDOMComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Prometheus Research, LLC 3 | */ 4 | 5 | import React, {PropTypes} from 'react'; 6 | import getComponentDisplayName from './getComponentDisplayName'; 7 | 8 | export default class StyleableDOMComponent extends React.Component { 9 | 10 | static Component = null; 11 | 12 | static stylesheet = null; 13 | 14 | static propTypes = { 15 | variant: PropTypes.object, 16 | className: PropTypes.string, 17 | Component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 18 | }; 19 | 20 | static style(spec) { 21 | return class extends StyleableDOMComponent { 22 | static displayName = getComponentDisplayName(this); 23 | static Component = this.Component; 24 | static stylesheet = this.stylesheet.override(spec); 25 | }; 26 | } 27 | 28 | render() { 29 | let { 30 | variant, 31 | className: extraClassName, 32 | Component = this.constructor.Component, 33 | ...props 34 | } = this.props; 35 | let className = this.constructor.stylesheet.asClassName(variant); 36 | if (extraClassName) { 37 | className = className + ' ' + extraClassName; 38 | } 39 | return ( 40 | 44 | ); 45 | } 46 | 47 | componentWillMount() { 48 | this.constructor.stylesheet.use(); 49 | } 50 | 51 | componentWillUnmount() { 52 | this.constructor.stylesheet.dispose(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | 3 | BIN = ./node_modules/.bin 4 | TESTS = $(shell find src -path '*/__tests__/*-test.js') 5 | SRC = $(filter-out $(TESTS), $(shell find src -name '*.js')) $(wildcard src/*.js.flow) 6 | LIB = $(SRC:src/%.js=lib/%.js) $(SRC:src/%.js.flow=lib/%.js.flow) lib/CSS.js.flow 7 | NODE = $(BIN)/babel-node $(BABEL_OPTIONS) 8 | MOCHA_OPTIONS = --require ./src/__tests__/setup.js 9 | MOCHA = $(BIN)/_mocha $(MOCHA_OPTIONS) 10 | NYC_OPTIONS = --all --require babel-core/register 11 | NYC = $(BIN)/nyc $(NYC_OPTIONS) 12 | 13 | build: 14 | @$(MAKE) -j 8 $(LIB) 15 | 16 | lint: 17 | @$(BIN)/eslint src 18 | 19 | test:: 20 | @NODE_ENV=test $(BIN)/babel-node $(MOCHA) -- $(TESTS) 21 | 22 | ci: 23 | @NODE_ENV=test $(BIN)/babel-node $(MOCHA) --watch -- $(TESTS) 24 | 25 | test-cov:: 26 | @NODE_ENV=test $(NYC) --check-coverage $(MOCHA) -- $(TESTS) 27 | 28 | test-flow:: 29 | @(cd test_flow/ && npm install && $(BIN)/flow check) 30 | 31 | report-cov:: 32 | @$(BIN)/nyc report --reporter html 33 | 34 | report-cov-coveralls:: 35 | @$(BIN)/nyc report --reporter=text-lcov | $(BIN)/coveralls 36 | 37 | version-major version-minor version-patch: lint test 38 | @npm version $(@:version-%=%) 39 | 40 | publish: build test lint 41 | @npm publish 42 | @git push --tags origin HEAD:master 43 | 44 | clean: 45 | @rm -rf lib/ 46 | 47 | lib/%.js: src/%.js 48 | @echo "Building $<" 49 | @mkdir -p $(@D) 50 | @$(BIN)/babel $(BABEL_OPTIONS) -o $@ $< 51 | 52 | lib/%.js.flow: src/%.js.flow 53 | @echo "Building $<" 54 | @mkdir -p $(@D) 55 | @cp $< $@ 56 | 57 | lib/CSS.js.flow: src/CSS.js 58 | @echo "Building $<" 59 | @mkdir -p $(@D) 60 | @cp $< $@ 61 | -------------------------------------------------------------------------------- /src/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Prometheus Research, LLC 3 | */ 4 | 5 | import React from 'react'; 6 | import {style, create, isStylesheet} from '../'; 7 | 8 | describe('index', function() { 9 | 10 | describe('style(...)', function() { 11 | 12 | it('creates a component with a stylesheet attached', function() { 13 | let Component = 'div'; 14 | let Styled = style(Component, {width: 10}); 15 | assert(Styled.Component === 'div'); 16 | assert(Styled.stylesheet.style.base.width === 10); 17 | assert(Styled.displayName === 'Styled(div)'); 18 | }); 19 | 20 | it('allows to override displayName', function() { 21 | let Component = 'div'; 22 | let Styled = style(Component, {width: 10}, 'CustomDisplayName'); 23 | assert(Styled.displayName === 'CustomDisplayName'); 24 | }); 25 | 26 | it('allows to override displayName via stylesheet spec', function() { 27 | let Component = 'div'; 28 | let Styled = style(Component, { 29 | width: 10, 30 | displayName: 'CustomDisplayName' 31 | }); 32 | assert(Styled.displayName === 'CustomDisplayName'); 33 | }); 34 | 35 | it('delegates to style() method if component has it', function() { 36 | class Component extends React.Component { 37 | 38 | render() { 39 | return
; 40 | } 41 | 42 | static style(_stylesheet) { 43 | return 'span'; 44 | } 45 | } 46 | 47 | let Styled = style(Component, {width: 10}); 48 | assert(Styled === 'span'); 49 | }); 50 | 51 | }); 52 | 53 | describe('create(...)', function() { 54 | 55 | it('creates a new stylesheet', function() { 56 | let stylesheet = create({width: 10}); 57 | assert(isStylesheet(stylesheet)); 58 | }); 59 | 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dom-stylesheet", 3 | "version": "0.8.8", 4 | "description": "Stylesheets for React DOM elements", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/prometheusresearch/react-dom-stylesheet.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "stylesheet", 16 | "dom" 17 | ], 18 | "babel": { 19 | "presets": [ 20 | "prometheusresearch" 21 | ] 22 | }, 23 | "eslintConfig": { 24 | "extends": "prometheusresearch" 25 | }, 26 | "nyc": { 27 | "exclude": [ 28 | "*.js", 29 | "lib/*.js", 30 | "coverage/*.js", 31 | "**/__tests__/*.js" 32 | ] 33 | }, 34 | "author": "Prometheus Research, LLC", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/prometheusresearch/react-dom-stylesheet/issues" 38 | }, 39 | "homepage": "https://github.com/prometheusresearch/react-dom-stylesheet#readme", 40 | "peerDependencies": { 41 | "react": "^0.14.0 || ^15.0.0" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.6.5", 45 | "babel-core": "^6.6.5", 46 | "babel-preset-prometheusresearch": "^0.1.0", 47 | "eslint": "^2.4.0", 48 | "eslint-config-prometheusresearch": "^0.2.0", 49 | "eslint-plugin-react": "^6.1.2", 50 | "flow-bin": "^0.32.0", 51 | "invariant": "^2.2.1", 52 | "jsdom": "^8.1.0", 53 | "mocha": "^2.4.5", 54 | "nyc": "^6.1.1", 55 | "power-assert": "^1.3.1", 56 | "react": "^0.14.0 || ^15.0.0", 57 | "react-dom": "^0.14.0 || ^15.0.0", 58 | "sinon": "^1.17.3", 59 | "react-addons-test-utils": "^15.4.1" 60 | }, 61 | "dependencies": { 62 | "inline-style-prefix-all": "^2.0.2", 63 | "lodash": "^4.13.1", 64 | "murmurhash-js": "^1.0.0", 65 | "react-css-property-operations": "^15.4.1", 66 | "style-loader": "^0.13.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Prometheus Research, LLC 3 | */ 4 | 5 | import {isStylesheet, create} from './DOMStylesheet'; 6 | import StyleableDOMComponent from './StyleableDOMComponent'; 7 | import getComponentDisplayName from './getComponentDisplayName'; 8 | 9 | export function style(Component, stylesheetSpec, options) { 10 | if (options == null) { 11 | options = {}; 12 | } else if (typeof options === 'string') { 13 | options = {displayName: options}; 14 | } 15 | 16 | let { 17 | displayName: displayNameFromSpec, 18 | ...stylesheet 19 | } = stylesheetSpec; 20 | 21 | options = { 22 | displayName: displayNameFromSpec, 23 | ...options 24 | }; 25 | 26 | if (Component.style) { 27 | return Component.style(stylesheet, options.displayName); 28 | } else { 29 | return wrapWithStylesheet(Component, stylesheet, options); 30 | } 31 | } 32 | 33 | /** 34 | * Produce a new component by applying a stylesheet. 35 | */ 36 | export function wrapWithStylesheet(Component, stylesheet, options) { 37 | if (options == null) { 38 | options = {}; 39 | } else if (typeof options === 'string') { 40 | options = {displayName: options}; 41 | } 42 | 43 | if (!isStylesheet(stylesheet)) { 44 | let id = options.displayName || getComponentDisplayName(Component); 45 | stylesheet = create(stylesheet, id); 46 | } 47 | return class extends StyleableDOMComponent { 48 | static displayName = getDisplayName(Component, options.displayName); 49 | static Component = Component; 50 | static stylesheet = stylesheet; 51 | }; 52 | } 53 | 54 | const STYLED_DISPLAY_NAME_RE = /^Styled\(.+\)$/; 55 | 56 | function getDisplayName(Component, displayName) { 57 | if (displayName != null) { 58 | return displayName; 59 | } 60 | displayName = getComponentDisplayName(Component); 61 | if (STYLED_DISPLAY_NAME_RE.exec(displayName)) { 62 | return displayName; 63 | } else { 64 | return `Styled(${displayName})`; 65 | } 66 | } 67 | 68 | export {StyleableDOMComponent, isStylesheet, create}; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React DOM Stylesheet 2 | 3 | A simple yet powerful way to define styled React DOM components. 4 | 5 | ## Installation 6 | 7 | ``` 8 | % npm install react-dom-stylesheet 9 | ``` 10 | 11 | ## Usage 12 | 13 | Basic usage: 14 | 15 | ``` 16 | import {style} from 'react-dom-stylesheet' 17 | 18 | let Label = style('span', { 19 | fontWeight: 'bold', 20 | fontSize: '12pt', 21 | }) 22 | ``` 23 | 24 | Now `Label` is a regular React component styled with `fontWeight` and 25 | `fontSize`. You can render into DOM and use as a part of React element tree: 26 | 27 | ``` 28 |