Removed GopherJS, basic frontend completed, need backend changes for

torrent storage
This commit is contained in:
2017-11-30 18:12:11 -05:00
parent 67fdef16b1
commit e98ad2cc88
69321 changed files with 5498914 additions and 337 deletions

View File

@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
### 1.2.1 (2017-11-12)
- Update React to v16
- Update brcast
### 1.1.0 (2017-07-12)
Add `themeListener`—advanced helper to hook theming in any Component.
### 1.0.2 (2017-07-12)
Fix a bug where ThemeProvider tried to merge with `outerTheme`, when there is none. In these case ThemeProvider needs to pass original theme without merging ([#19][] by [@iamstarkov][])
[#19]: https://github.com/iamstarkov/theming/pull/19
[@iamstarkov]: https://github.com/iamstarkov/
### 1.0.1 (2017-06-11)
Fix a bug with webpack resolving `pkg.modules`, so from now own `theming` has commonjs (`dist/cjs`) and es modules (`dist/esm`) entry poingts.
### 1.0.0 (2017-06-09)
Initial release with `channel`, `ThemeProvider`, `withTheme` and `createTheming`

View File

@@ -0,0 +1,373 @@
# theming
[![Greenkeeper badge](https://badges.greenkeeper.io/iamstarkov/theming.svg)](https://greenkeeper.io/)
[![NPM version][npm-image]][npm-url]
[![Build][travis-image]][travis-url]
[![Coveralls Status][coveralls-image]][coveralls-url]
[![Dependency Status][depstat-image]][depstat-url]
> Unified CSSinJS theming solution for React
* `ThemeProvider` allows you to pass, update, merge and augment `theme` through context down react tree.
* `withTheme` allows you to receive theme and its updates in your components as a `theme` prop.
* `createTheming` allows you to integrate `theming` into your CSSinJS library with custom `channel` (if you need custom one).
* _Advanced usage:_ `themeListener` allows you to add theming support in your components.
See [Motivation](#motivation) for details.
## Table of Contents
* [Install](#install)
* [Usage](#usage)
* [Playground demo](#playground-demo)
* [Motivation](#motivation)
* [API](#api)
* [channel](#channel)
* [ThemeProvider](#themeprovider)
* [withTheme](#withthemecomponent)
* [themeListener](#themeListener)
* [createTheming](#createthemingcustomchannel)
* [Credits](#credits)
* [License](#license)
## Install
npm install --save theming
# or
yarn add theming
## Usage
In your components
Note: this component i will use later to show what theme you will get
```jsx
import React from 'react';
import { withTheme } from 'theming';
const DemoBox = props => {
console.log(props.theme);
return (<div />);
}
export default withTheme(DemoBox);
```
In your app
```jsx
import React from 'react';
import { ThemeProvider } from 'theming';
import DemoBox from './components/DemoBox'
const theme = {
color: 'black',
background: 'white',
};
const App = () => (
<ThemeProvider theme={theme}>
<DemoBox /> {/* { color: 'black', background: 'white' } */}
</ThemeProvider>
)
export default App;
```
## Playground demo
Be our guest, play with `theming` in this webpackbin:
[https://www.webpackbin.com/bins/-Km8TglfWP84oYhDquT1](https://www.webpackbin.com/bins/-Km8TglfWP84oYhDquT1)
![theming playground demo](https://user-images.githubusercontent.com/559321/27082371-ba194816-5044-11e7-8f06-6cbdbdefb602.gif)
## Motivation
These components are enabling seamless theming for your react applications. And as far as you dont want to pass theme object to each and every component. Thats why you want to use context. But as far context feature is _experimental API and it is likely to break in future releases of React_ you don't want to use it directly. Here `theming` comes to play.
> If you insist on using context despite these warnings, try to isolate your use of context to a small area and avoid using the context API directly when possible so that it's easier to upgrade when the API changes.
>
> If you insist on using context despite these warnings, try to isolate your use of context to a small area and avoid using the context API directly when possible so that it's easier to upgrade when the API changes.
> — [Context, React documentation](https://facebook.github.io/react/docs/context.html)
Regarding _isolation your use of context to a small area_ and _small areas__ in particular our very own react prophet Dan Abramov have a thing to say:
> Should I use React unstable “context” feature?
> <img src="https://pbs.twimg.com/media/CmeGPNcVYAAM7TR.jpg" alt="![context application areas]" height="300" />
> — [Dan Abramov @dan_abramov on Twitter](https://twitter.com/dan_abramov/status/749715530454622208?lang=en)
So you are fine to use context for theming. `theming` package provides everything you need to do that:
* `ThemeProvider` allows you to pass and update `theme` through context down react tree.
* `withTheme` allows you to receive theme and its updates in your components as a `theme` prop.
* `createTheming` allows you to integrate `theming` into your CSSinJS library with custom `channel` (if you need custom one).
## API
### channel
Theming package by default uses this string as a name of the field in context (hence `contextTypes` and `childContextTypes`). If you want to build your own components on top of theming, it might be a good idea to not rely on hard coded value, but instead import this value from the package.
```js
import { channel } from 'theming';
console.log(channel); '__THEMING__';
```
### ThemeProvider
React High-Order component, which passes theme object down the react tree by context.
```jsx
import { ThemeProvider } from 'theming';
const theme = { /*…*/ };
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
```
#### props
##### props.theme
*Required*
Type: `Object`, `Function`
If its `Object` and its root `ThemeProvider` then its intact and being passed down the react tree.
```jsx
const theme = { themed: true };
<ThemeProvider theme={theme}>
<DemoBox /> {/* { themed: true } */}
</ThemeProvider>
```
If its `Object` and its nested `ThemeProvider` then its being merged with theme from parent `ThemeProvider` and passed down to the react tree.
```jsx
const theme = { themed: true };
const patch = { merged: true };
<ThemeProvider theme={theme}>
<ThemeProvider theme={patch}>
<DemoBox /> {/* { themed: true, merged: true } */}
</ThemeProvider>
</ThemeProvider>
```
If its `Function` and its nested `ThemeProvider` then its being applied to the theme from parent `ThemeProvider`. if result is an `Object` it will be passed down to the react tree, throws otherwise.
```jsx
const theme = { themed: true };
const augment = outerTheme =>
Object.assign({}, outerTheme, { augmented: true });
<ThemeProvider theme={theme}>
<ThemeProvider theme={augment}>
<DemoBox /> {/* { themed: true, augmented: true } */}
</ThemeProvider>
</ThemeProvider>
```
##### props.children
*Required*
Type: `PropTypes.element`
ThemeProvider uses [`React.Children.only`](https://facebook.github.io/react/docs/react-api.html#react.children.only) in render, which returns the only child in children. Throws otherwise.
### withTheme(component)
React High-Order component, which maps context to theme prop.
#### component
*Required*
Type: `PropTypes.element`
You need to have `ThemeProvider` with a theme somewhere upper the react tree, after that wrap your component in `withTheme` and your component will get theme as a prop. `withTheme` will handle initial theme object as well as theme updates.
PS. It doesnt break if you have `PureComponent` somewhere in between your ThemeProvider and withTheme (i have tests for that).
Usage with Component:
```jsx
import React from 'react';
import { withTheme } from 'theming';
const DemoBox = props => {
console.log(props.theme);
return (<div />);
}
export default withTheme(DemoBox);
```
In the app:
```jsx
import React from 'react';
import { ThemeProvider } from 'theming';
import DemoBox from './components/DemoBox'
const theme = {
color: 'black',
background: 'white',
};
const App = () => (
<ThemeProvider theme={theme}>
<DemoBox /> {/* { color: 'black', background: 'white' } */}
</ThemeProvider>
)
export default App;
```
### themeListener
Advanced helper to hook theming in any Component.
```js
import { themeListener } from 'theming';
function CustomWithTheme(Component) {
return class CustomWithTheme extends React.Component {
static contextTypes = themeListener.contextTypes;
constructor(props, context) {
super(props, context);
this.state = { theme: themeListener.initial(context) };
this.setTheme = theme => this.setState({ theme });
}
componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, this.setTheme);
}
componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe();
}
}
render() {
const { theme } = this.state;
return <Component theme={theme} {...this.props} />;
}
}
}
```
themeListener is an `Object` with following fields:
* `themeListener.contextTypes`
* type: `Object`
* meant to be added your component's contextTypes:
```js
static contextTypes = themeListener.contextTypes;
// or
static contextTypes = Object.assign({}, themeListener.contextTypes, {
/* your Component's contextTypes */
});
```
* `themeListener.initial`
* type: `Function`
* takes a single context `Object`, where `context` is `context` of your component
* meant to be used in `constructor`
* throws an error if your component will be used outside ThemeProvider
* example:
```js
constructor(props, context) {
super(props, context);
this.state = { theme: themeListener.initial(context) }
}
```
* `themeListener.subscribe`
* type: `Function`
* takes 2 arguments:
* context `Object`, where `context` is `this.context` from your component
* callback `Function`, which in turn will be invoked with theme update `Object`, every time theme is updated in `ThemeProvider`
* meant to be used in `componentDidMount`
* returns unsubscribe `Function`, which you should invoke in `componentWillUnmount`
* example:
```js
componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, theme => this.setState({ theme }));
}
componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe();
}
}
```
### createTheming(customChannel)
Function to create `ThemeProvider` and `withTheme` with custom context channel.
#### customChannel
Type: `String`
Default: `__THEMING__`
Result: `Object { channel, withTheme, ThemeProvider. themeListener }`
`withTheme`, `ThemeProvider` and `themeListener` are the same as default ones, but with overwritten context channel.
`channel` is `customChannel` to track what is context channel.
```js
import { createTheming } from 'theming';
const theming = createTheming('__styled-components__');
const { channel, withTheme, ThemeProvider, themeListener } = theming;
export default {
channel,
withTheme,
ThemeProvider,
themeListener,
};
```
## Credits
* Thanks to [jss][jss] creator [Oleg Slobodskoi @kof][kof] for immersive help, support and code review.
* Thanks to [styled-components][sc] creator [Max Stoiber @mxstbr][mxstbr] for initial and battle tested implementation of theming support in [styled-components][sc] as well as help and code review.
* Thanks to [styled-components'][sc] core team member [Phil Plückthun @philpl][philpl] for help and code review.
* Thanks to [glamorous][glamorous] creator [Kent C. Dodds @kentcdodds][kentcdodds] for help, support and code review.
* Thanks to [glamorous's][glamorous] core team member [Alessandro Arnodo @vesparny][vesparny] for improved theming support in [glamorous][glamorous] and [brcast][brcast].
* Thanks to [Gert Sallaerts @gertt][gertt] for the [playground][playground] demo.
[kof]: https://github.com/kof
[mxstbr]: https://github.com/mxstbr
[philpl]: https://github.com/philpl
[kentcdodds]: https://github.com/kentcdodds
[vesparny]: https://github.com/vesparny
[gertt]: https://github.com/gertt
[jss]: https://github.com/cssinjs/jss
[sc]: https://github.com/styled-components/styled-components
[glamorous]: https://github.com/paypal/glamorous
[brcast]: https://github.com/vesparny/brcast
[playground]: https://www.webpackbin.com/bins/-Km8TglfWP84oYhDquT1
## License
MIT © [Vladimir Starkov](https://iamstarkov.com)
[npm-url]: https://npmjs.org/package/theming
[npm-image]: https://img.shields.io/npm/v/theming.svg?style=flat-square
[travis-url]: https://travis-ci.org/iamstarkov/theming
[travis-image]: https://img.shields.io/travis/iamstarkov/theming.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/iamstarkov/theming
[coveralls-image]: https://img.shields.io/coveralls/iamstarkov/theming.svg?style=flat-square
[depstat-url]: https://david-dm.org/iamstarkov/theming
[depstat-image]: https://david-dm.org/nordnet/grid.svg?style=flat-square

View File

@@ -0,0 +1,6 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = '__THEMING__';

View File

@@ -0,0 +1,44 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = createThemeListener;
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _channel = require('./channel');
var _channel2 = _interopRequireDefault(_channel);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function createThemeListener() {
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _channel2.default;
var contextTypes = _defineProperty({}, CHANNEL, _propTypes2.default.object.isRequired);
function initial(context) {
if (!context[CHANNEL]) {
throw new Error('[' + this.displayName + '] Please use ThemeProvider to be able to use WithTheme');
}
return context[CHANNEL].getState();
}
function subscribe(context, cb) {
if (context[CHANNEL]) {
return context[CHANNEL].subscribe(cb);
}
}
return {
contextTypes: contextTypes,
initial: initial,
subscribe: subscribe
};
}

View File

@@ -0,0 +1,155 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
exports.default = createThemeProvider;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _isFunction = require('is-function');
var _isFunction2 = _interopRequireDefault(_isFunction);
var _isPlainObject = require('is-plain-object');
var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
var _channel = require('./channel');
var _channel2 = _interopRequireDefault(_channel);
var _brcast = require('brcast');
var _brcast2 = _interopRequireDefault(_brcast);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/**
* Provide a theme to an entire react component tree via context
* and event listeners (have to do both context
* and event emitter as pure components block context updates)
*/
function createThemeProvider() {
var _class, _temp2;
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _channel2.default;
return _temp2 = _class = function (_React$Component) {
_inherits(ThemeProvider, _React$Component);
function ThemeProvider() {
var _ref;
var _temp, _this, _ret;
_classCallCheck(this, ThemeProvider);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ThemeProvider.__proto__ || Object.getPrototypeOf(ThemeProvider)).call.apply(_ref, [this].concat(args))), _this), _this.broadcast = (0, _brcast2.default)(_this.getTheme()), _this.setOuterTheme = function (theme) {
_this.outerTheme = theme;
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(ThemeProvider, [{
key: 'getTheme',
// Get the theme from the props, supporting both (outerTheme) => {} as well as object notation
value: function getTheme(passedTheme) {
var theme = passedTheme || this.props.theme;
if ((0, _isFunction2.default)(theme)) {
var mergedTheme = theme(this.outerTheme);
if (!(0, _isPlainObject2.default)(mergedTheme)) {
throw new Error('[ThemeProvider] Please return an object from your theme function, i.e. theme={() => ({})}!');
}
return mergedTheme;
}
if (!(0, _isPlainObject2.default)(theme)) {
throw new Error('[ThemeProvider] Please make your theme prop a plain object');
}
if (!this.outerTheme) {
return theme;
}
return _extends({}, this.outerTheme, theme);
}
}, {
key: 'getChildContext',
value: function getChildContext() {
return _defineProperty({}, CHANNEL, this.broadcast);
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
// create a new subscription for keeping track of outer theme, if present
if (this.context[CHANNEL]) {
this.subscriptionId = this.context[CHANNEL].subscribe(this.setOuterTheme);
}
}
// set broadcast state by merging outer theme with own
}, {
key: 'componentWillMount',
value: function componentWillMount() {
if (this.context[CHANNEL]) {
this.setOuterTheme(this.context[CHANNEL].getState());
this.broadcast.setState(this.getTheme());
}
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
this.broadcast.setState(this.getTheme(nextProps.theme));
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.subscriptionId !== undefined) {
this.context[CHANNEL].unsubscribe(this.subscriptionId);
delete this.subscriptionId;
}
}
}, {
key: 'render',
value: function render() {
if (!this.props.children) {
return null;
}
return _react2.default.Children.only(this.props.children);
}
}]);
return ThemeProvider;
}(_react2.default.Component), _class.propTypes = {
children: _propTypes2.default.element,
theme: _propTypes2.default.oneOfType([_propTypes2.default.shape({}), _propTypes2.default.func]).isRequired
}, _class.childContextTypes = _defineProperty({}, CHANNEL, _propTypes2.default.object.isRequired), _class.contextTypes = _defineProperty({}, CHANNEL, _propTypes2.default.object), _temp2;
}

View File

@@ -0,0 +1,84 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
exports.default = createWithTheme;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _channel = require('./channel');
var _channel2 = _interopRequireDefault(_channel);
var _createThemeListener = require('./create-theme-listener');
var _createThemeListener2 = _interopRequireDefault(_createThemeListener);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var getDisplayName = function getDisplayName(Component) {
return Component.displayName || Component.name || 'Component';
};
function createWithTheme() {
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _channel2.default;
var themeListener = (0, _createThemeListener2.default)(CHANNEL);
return function (Component) {
var _class, _temp;
return _temp = _class = function (_React$Component) {
_inherits(WithTheme, _React$Component);
function WithTheme(props, context) {
_classCallCheck(this, WithTheme);
var _this = _possibleConstructorReturn(this, (WithTheme.__proto__ || Object.getPrototypeOf(WithTheme)).call(this, props, context));
_this.state = { theme: themeListener.initial(context) };
_this.setTheme = function (theme) {
return _this.setState({ theme: theme });
};
return _this;
}
_createClass(WithTheme, [{
key: 'componentDidMount',
value: function componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, this.setTheme);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe();
}
}
}, {
key: 'render',
value: function render() {
var theme = this.state.theme;
return _react2.default.createElement(Component, _extends({ theme: theme }, this.props));
}
}]);
return WithTheme;
}(_react2.default.Component), _class.displayName = 'WithTheme(' + getDisplayName(Component) + ')', _class.contextTypes = themeListener.contextTypes, _temp;
};
}

View File

@@ -0,0 +1,48 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.themeListener = exports.ThemeProvider = exports.withTheme = exports.channel = undefined;
exports.createTheming = createTheming;
var _createThemeProvider = require('./create-theme-provider');
var _createThemeProvider2 = _interopRequireDefault(_createThemeProvider);
var _createWithTheme = require('./create-with-theme');
var _createWithTheme2 = _interopRequireDefault(_createWithTheme);
var _createThemeListener = require('./create-theme-listener');
var _createThemeListener2 = _interopRequireDefault(_createThemeListener);
var _channel = require('./channel');
var _channel2 = _interopRequireDefault(_channel);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var channel = exports.channel = _channel2.default;
var withTheme = exports.withTheme = (0, _createWithTheme2.default)();
var ThemeProvider = exports.ThemeProvider = (0, _createThemeProvider2.default)();
var themeListener = exports.themeListener = (0, _createThemeListener2.default)();
function createTheming() {
var customChannel = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _channel2.default;
return {
channel: customChannel,
withTheme: (0, _createWithTheme2.default)(customChannel),
ThemeProvider: (0, _createThemeProvider2.default)(customChannel),
themeListener: (0, _createThemeListener2.default)(customChannel)
};
}
exports.default = {
channel: _channel2.default,
withTheme: withTheme,
ThemeProvider: ThemeProvider,
themeListener: themeListener,
createTheming: createTheming
};

View File

@@ -0,0 +1 @@
export default '__THEMING__';

View File

@@ -0,0 +1,30 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import PropTypes from 'prop-types';
import channel from './channel';
export default function createThemeListener() {
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : channel;
var contextTypes = _defineProperty({}, CHANNEL, PropTypes.object.isRequired);
function initial(context) {
if (!context[CHANNEL]) {
throw new Error('[' + this.displayName + '] Please use ThemeProvider to be able to use WithTheme');
}
return context[CHANNEL].getState();
}
function subscribe(context, cb) {
if (context[CHANNEL]) {
return context[CHANNEL].subscribe(cb);
}
}
return {
contextTypes: contextTypes,
initial: initial,
subscribe: subscribe
};
}

View File

@@ -0,0 +1,128 @@
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
import React from 'react';
import PropTypes from 'prop-types';
import isFunction from 'is-function';
import isPlainObject from 'is-plain-object';
import channel from './channel';
import createBroadcast from 'brcast';
/**
* Provide a theme to an entire react component tree via context
* and event listeners (have to do both context
* and event emitter as pure components block context updates)
*/
export default function createThemeProvider() {
var _class, _temp2;
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : channel;
return _temp2 = _class = function (_React$Component) {
_inherits(ThemeProvider, _React$Component);
function ThemeProvider() {
var _ref;
var _temp, _this, _ret;
_classCallCheck(this, ThemeProvider);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ThemeProvider.__proto__ || Object.getPrototypeOf(ThemeProvider)).call.apply(_ref, [this].concat(args))), _this), _this.broadcast = createBroadcast(_this.getTheme()), _this.setOuterTheme = function (theme) {
_this.outerTheme = theme;
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(ThemeProvider, [{
key: 'getTheme',
// Get the theme from the props, supporting both (outerTheme) => {} as well as object notation
value: function getTheme(passedTheme) {
var theme = passedTheme || this.props.theme;
if (isFunction(theme)) {
var mergedTheme = theme(this.outerTheme);
if (!isPlainObject(mergedTheme)) {
throw new Error('[ThemeProvider] Please return an object from your theme function, i.e. theme={() => ({})}!');
}
return mergedTheme;
}
if (!isPlainObject(theme)) {
throw new Error('[ThemeProvider] Please make your theme prop a plain object');
}
if (!this.outerTheme) {
return theme;
}
return _extends({}, this.outerTheme, theme);
}
}, {
key: 'getChildContext',
value: function getChildContext() {
return _defineProperty({}, CHANNEL, this.broadcast);
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
// create a new subscription for keeping track of outer theme, if present
if (this.context[CHANNEL]) {
this.subscriptionId = this.context[CHANNEL].subscribe(this.setOuterTheme);
}
}
// set broadcast state by merging outer theme with own
}, {
key: 'componentWillMount',
value: function componentWillMount() {
if (this.context[CHANNEL]) {
this.setOuterTheme(this.context[CHANNEL].getState());
this.broadcast.setState(this.getTheme());
}
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
this.broadcast.setState(this.getTheme(nextProps.theme));
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.subscriptionId !== undefined) {
this.context[CHANNEL].unsubscribe(this.subscriptionId);
delete this.subscriptionId;
}
}
}, {
key: 'render',
value: function render() {
if (!this.props.children) {
return null;
}
return React.Children.only(this.props.children);
}
}]);
return ThemeProvider;
}(React.Component), _class.propTypes = {
children: PropTypes.element,
theme: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.func]).isRequired
}, _class.childContextTypes = _defineProperty({}, CHANNEL, PropTypes.object.isRequired), _class.contextTypes = _defineProperty({}, CHANNEL, PropTypes.object), _temp2;
}

View File

@@ -0,0 +1,66 @@
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
import React from 'react';
import channel from './channel';
import createThemeListener from './create-theme-listener';
var getDisplayName = function getDisplayName(Component) {
return Component.displayName || Component.name || 'Component';
};
export default function createWithTheme() {
var CHANNEL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : channel;
var themeListener = createThemeListener(CHANNEL);
return function (Component) {
var _class, _temp;
return _temp = _class = function (_React$Component) {
_inherits(WithTheme, _React$Component);
function WithTheme(props, context) {
_classCallCheck(this, WithTheme);
var _this = _possibleConstructorReturn(this, (WithTheme.__proto__ || Object.getPrototypeOf(WithTheme)).call(this, props, context));
_this.state = { theme: themeListener.initial(context) };
_this.setTheme = function (theme) {
return _this.setState({ theme: theme });
};
return _this;
}
_createClass(WithTheme, [{
key: 'componentDidMount',
value: function componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, this.setTheme);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe();
}
}
}, {
key: 'render',
value: function render() {
var theme = this.state.theme;
return React.createElement(Component, _extends({ theme: theme }, this.props));
}
}]);
return WithTheme;
}(React.Component), _class.displayName = 'WithTheme(' + getDisplayName(Component) + ')', _class.contextTypes = themeListener.contextTypes, _temp;
};
}

View File

@@ -0,0 +1,27 @@
import createThemeProvider from './create-theme-provider';
import createWithTheme from './create-with-theme';
import createThemeListener from './create-theme-listener';
import defaultChannel from './channel';
export var channel = defaultChannel;
export var withTheme = createWithTheme();
export var ThemeProvider = createThemeProvider();
export var themeListener = createThemeListener();
export function createTheming() {
var customChannel = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultChannel;
return {
channel: customChannel,
withTheme: createWithTheme(customChannel),
ThemeProvider: createThemeProvider(customChannel),
themeListener: createThemeListener(customChannel)
};
}
export default {
channel: defaultChannel,
withTheme: withTheme,
ThemeProvider: ThemeProvider,
themeListener: themeListener,
createTheming: createTheming
};

View File

@@ -0,0 +1,138 @@
{
"_args": [
[
"theming@1.2.1",
"C:\\Users\\deranjer\\GoglandProjects\\torrent-project\\torrent-project"
]
],
"_from": "theming@1.2.1",
"_id": "theming@1.2.1",
"_inBundle": false,
"_integrity": "sha512-cGFWBqPP93I/pVucq6OAYQFoYyvueEOUmK5dxdlFsgx/fzn8In0sbeLEgxvEHk1ktvHGeX4uZFP8QJmXyIzw/A==",
"_location": "/material-ui/theming",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "theming@1.2.1",
"name": "theming",
"escapedName": "theming",
"rawSpec": "1.2.1",
"saveSpec": null,
"fetchSpec": "1.2.1"
},
"_requiredBy": [
"/material-ui/react-jss"
],
"_resolved": "https://registry.npmjs.org/theming/-/theming-1.2.1.tgz",
"_spec": "1.2.1",
"_where": "C:\\Users\\deranjer\\GoglandProjects\\torrent-project\\torrent-project",
"author": {
"name": "Vladimir Starkov",
"email": "iamstarkov@gmail.com",
"url": "https://iamstarkov.com"
},
"ava": {
"files": [
"src/**/*test.js"
],
"require": [
"babel-register",
"./.browser-env"
],
"babel": "inherit"
},
"bugs": {
"url": "https://github.com/iamstarkov/theming/issues"
},
"dependencies": {
"brcast": "^3.0.1",
"is-function": "^1.0.1",
"is-plain-object": "^2.0.1",
"prop-types": "^15.5.8"
},
"description": "Unified CSSinJS theming solution for React",
"devDependencies": {
"ava": "^0.22.0",
"babel-cli": "^6.24.1",
"babel-eslint": "^8.0.1",
"babel-preset-env": "^1.4.0",
"babel-preset-es2017": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.24.1",
"browser-env": "^3.2.1",
"coveralls": "3.0.0",
"cross-env": "^5.0.1",
"enzyme": "^3.1.0",
"enzyme-adapter-react-16": "^1.0.2",
"eslint": "^4.1.0",
"eslint-config-pedant": "^0.10.0",
"eslint-config-prettier": "^2.1.0",
"eslint-plugin-react": "^7.0.1",
"eslint-plugin-require-path-exists": "^1.1.7",
"npm-run-all": "^4.0.2",
"nyc": "^11.0.2",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-test-renderer": "^16.0.0",
"rimraf": "^2.6.1"
},
"files": [
"src",
"dist"
],
"homepage": "https://github.com/iamstarkov/theming#readme",
"keywords": [
"react",
"theme",
"theming",
"styled-components",
"jss",
"cssinjs",
"css-in-js"
],
"license": "MIT",
"main": "dist/cjs",
"module": "dist/esm",
"name": "theming",
"nyc": {
"include": [
"src"
],
"exclude": [
"src/*test*"
],
"all": true,
"cache": true,
"reporter": [
"lcov",
"text"
]
},
"peerDependencies": {
"react": ">=0.15"
},
"repository": {
"type": "git",
"url": "git+https://github.com/iamstarkov/theming.git"
},
"scripts": {
"babel:cjs": "cross-env BABEL_ENV=cjs babel src -d dist/cjs --ignore '*test*'",
"babel:esm": "cross-env BABEL_ENV=esm babel src -d dist/esm --ignore '*test*'",
"babel:watch:cjs": "npm run babel:cjs -- --watch",
"babel:watch:esm": "npm run babel:esm -- --watch",
"build": "run-s clean babel:*",
"clean": "rimraf dist",
"coverage": "cross-env BABEL_ENV=cjs nyc ava",
"coveralls": "run-s coveralls:*",
"coveralls:gather": "npm run coverage",
"coveralls:upload": "coveralls < coverage/lcov.info",
"lint": "eslint .",
"postversion": "npm run build",
"tdd": "npm run test -- --watch",
"test": "cross-env BABEL_ENV=cjs ava",
"watch": "npm-run-all clean -p babel:watch:*"
},
"version": "1.2.1"
}

View File

@@ -0,0 +1 @@
export default '__THEMING__';

View File

@@ -0,0 +1,30 @@
import PropTypes from 'prop-types';
import channel from './channel';
export default function createThemeListener(CHANNEL = channel) {
const contextTypes = {
[CHANNEL]: PropTypes.object.isRequired,
};
function initial(context) {
if (!context[CHANNEL]) {
throw new Error(
`[${this.displayName}] Please use ThemeProvider to be able to use WithTheme`,
);
}
return context[CHANNEL].getState()
}
function subscribe(context, cb) {
if (context[CHANNEL]) {
return context[CHANNEL].subscribe(cb);
}
}
return {
contextTypes,
initial,
subscribe,
};
}

View File

@@ -0,0 +1,248 @@
import test from 'ava';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import createBroadcast from 'brcast';
import { mount, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import isFunction from 'is-function';
import isPainObject from 'is-plain-object';
import { getChannel, Pure, mountOptions, getInterceptor } from './test-helpers';
import CHANNEL from './channel';
import createThemeListener from './create-theme-listener';
configure({ adapter: new Adapter() });
test(`createThemeListener's type`, t => {
const actual = isFunction(createThemeListener);
t.true(actual, `createThemeListener should be a function`);
});
test(`createThemeListener's result's type`, t => {
const actual = isPainObject(createThemeListener());
t.true(actual, `createThemeListener() should be an object`);
});
test(`themeListener's fields`, t => {
const actual = Object.keys(createThemeListener());
const expected = ['contextTypes', 'initial', 'subscribe'];
t.deepEqual(
actual,
expected,
`themeListener should have contextTypes and bind fields`,
);
});
test(`themeListener's default channel`, t => {
const themeListener = createThemeListener();
const actual = getChannel(themeListener);
const expected = CHANNEL;
t.is(actual, expected, `themeListener should use default channel by default`);
});
test(`themeListener's custom channel`, t => {
const customChannel = '__CUSTOM__';
const themeListener = createThemeListener(customChannel);
const actual = getChannel(themeListener);
const expected = customChannel;
t.is(
actual,
expected,
`themeListener should have custom channel if one is passed`,
);
});
test(`themeListener's initial and subscribe`, t => {
const themeListener = createThemeListener();
const { initial, subscribe } = themeListener;
const actual = [initial, subscribe].every(isFunction);
t.true(
actual,
`themeListener's init, subscribe and unsubscribe should be a function`,
);
});
const getTrap = themeListener => {
return class ThemeListenerTrap extends Component {
static propTypes = {
intercept: PropTypes.func.isRequired,
};
static contextTypes = themeListener.contextTypes;
constructor(props, context) {
super(props, context);
this.props.intercept(themeListener.initial(context))
}
componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, this.props.intercept)
}
componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe()
}
}
// eslint-disable-next-line
render() {
return <div />;
}
};
};
test(`themeListener without ThemeProvider`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
t.throws(
() => {
mount(<Trap intercept={() => {}} />);
},
Error,
`themeListener should throw if used without appropriate context`,
);
});
test(`themeListener and init`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor();
const expected = theme;
mount(<Trap intercept={actual} />, mountOptions(broadcast));
t.deepEqual(actual(), expected, 'init should get initial theme');
});
test(`themeListener, init and nested react tree`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor();
const expected = theme;
mount(
<div>
<div>
<Trap intercept={actual} />
</div>
</div>,
mountOptions(broadcast),
);
t.deepEqual(
actual(),
expected,
'init should get initial theme through nested react tree',
);
});
test(`themeListener, init and PureComponent`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor();
const expected = theme;
mount(
<Pure>
<Trap intercept={actual} />
</Pure>,
mountOptions(broadcast),
);
t.deepEqual(
actual(),
expected,
'init should get initial theme through PureComponent',
);
});
test(`themeListener and subscribe`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const update = { updated: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor(theme);
const expected = update;
mount(<Trap intercept={actual} />, mountOptions(broadcast));
broadcast.setState(update);
t.deepEqual(actual(), expected, 'subscribe should get theme update');
});
test(`themeListener, subscribe and nested react tree`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const update = { updated: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor(theme);
const expected = update;
mount(
<div>
<div>
<Trap intercept={actual} />
</div>
</div>,
mountOptions(broadcast),
);
broadcast.setState(update);
t.deepEqual(
actual(),
expected,
'subscribe should get theme update through nested tree',
);
});
test(`themeListener, subscribe and PureComponent`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const update = { updated: true };
const broadcast = createBroadcast(theme);
const actual = getInterceptor(theme);
const expected = update;
mount(
<Pure>
<Trap intercept={actual} />
</Pure>,
mountOptions(broadcast),
);
broadcast.setState(update);
t.deepEqual(
actual(),
expected,
'subscribe should get theme update through PureComponent',
);
});
test(`themeListener and unsubscribe`, t => {
const themeListener = createThemeListener();
const Trap = getTrap(themeListener);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const unsubscribed = getInterceptor(false);
const wrapper = mount(<Trap intercept={() => {}} />, mountOptions(broadcast));
wrapper.instance().unsubscribe = () => unsubscribed(true);
t.false(unsubscribed());
wrapper.unmount();
t.true(unsubscribed(), 'unsubscribe should happen on unmount');
});

View File

@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import isFunction from 'is-function';
import isPlainObject from 'is-plain-object';
import channel from './channel';
import createBroadcast from 'brcast';
/**
* Provide a theme to an entire react component tree via context
* and event listeners (have to do both context
* and event emitter as pure components block context updates)
*/
export default function createThemeProvider(CHANNEL = channel) {
return class ThemeProvider extends React.Component {
static propTypes = {
children: PropTypes.element,
theme: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.func])
.isRequired,
};
static childContextTypes = {
[CHANNEL]: PropTypes.object.isRequired,
};
static contextTypes = {
[CHANNEL]: PropTypes.object,
};
broadcast = createBroadcast(this.getTheme());
// Get the theme from the props, supporting both (outerTheme) => {} as well as object notation
getTheme(passedTheme) {
const theme = passedTheme || this.props.theme;
if (isFunction(theme)) {
const mergedTheme = theme(this.outerTheme);
if (!isPlainObject(mergedTheme)) {
throw new Error(
'[ThemeProvider] Please return an object from your theme function, i.e. theme={() => ({})}!',
);
}
return mergedTheme;
}
if (!isPlainObject(theme)) {
throw new Error(
'[ThemeProvider] Please make your theme prop a plain object',
);
}
if (!this.outerTheme) {
return theme;
}
return { ...this.outerTheme, ...theme };
}
setOuterTheme = theme => {
this.outerTheme = theme;
};
getChildContext() {
return { [CHANNEL]: this.broadcast };
}
componentDidMount() {
// create a new subscription for keeping track of outer theme, if present
if (this.context[CHANNEL]) {
this.subscriptionId = this.context[CHANNEL].subscribe(this.setOuterTheme);
}
}
// set broadcast state by merging outer theme with own
componentWillMount() {
if (this.context[CHANNEL]) {
this.setOuterTheme(this.context[CHANNEL].getState());
this.broadcast.setState(this.getTheme());
}
}
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
this.broadcast.setState(this.getTheme(nextProps.theme));
}
}
componentWillUnmount() {
if (this.subscriptionId !== undefined) {
this.context[CHANNEL].unsubscribe(this.subscriptionId);
delete this.subscriptionId;
}
}
render() {
if (!this.props.children) {
return null;
}
return React.Children.only(this.props.children);
}
};
}

View File

@@ -0,0 +1,259 @@
import test from 'ava';
import React, { Component } from 'react';
import { mount, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import isFunction from 'is-function';
import createThemeProvider from './create-theme-provider';
import channel from './channel';
// import createBroadcast from './create-broadcast';
const createBroadcast = require('brcast');
configure({ adapter: new Adapter() });
import {
getChannel,
Trap,
Pure,
getInterceptor,
mountOptions,
} from './test-helpers';
test(`createThemeProvider's type`, t => {
const actual = isFunction(createThemeProvider);
t.true(actual, `createThemeProvider should be a function`);
});
test(`createThemeProvider's result instance type`, t => {
const ThemeProvider = createThemeProvider();
const actual = Component.isPrototypeOf(ThemeProvider);
t.true(actual, `createThemeProvider() should be a React Component`);
});
test(`ThemeProvider default channel`, t => {
const ThemeProvider = createThemeProvider();
const actual = getChannel(ThemeProvider);
const expected = channel;
t.is(actual, expected, `createThemeProvider() should have default channel`);
});
test(`ThemeProvider custom channel`, t => {
const custom = '__CUSTOM__';
const ThemeProvider = createThemeProvider(custom);
const actual = getChannel(ThemeProvider);
const expected = custom;
t.is(
actual,
expected,
`createThemeProvider() should have custom channel if one is provided`,
);
});
test(`ThemeProvider unsubscribes on unmounting`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const wrapper = mount(
<ThemeProvider theme={theme} />,
mountOptions(broadcast),
);
const { subscriptionId } = wrapper.instance();
t.true(wrapper.instance().subscriptionId !== undefined, 'brcast subscriptionId is undefined');
t.true(typeof wrapper.instance().subscriptionId === 'number', 'brcast subscriptionId expected to be number');
const subscription = getInterceptor(subscriptionId);
const brcastInst = wrapper.context(channel);
brcastInst.unsubscribe = (id) => subscription(id);
wrapper.setContext({[channel]: brcastInst});
wrapper.unmount();
t.true(subscription() === subscriptionId, `ThemeProvider should unsubscribe on unmounting`);
});
test(`ThemeProvider and not a plain object theme`, t => {
const ThemeProvider = createThemeProvider();
t.throws(
() => {
mount(<ThemeProvider theme={false} />);
},
Error,
`ThemeProvider should throw if theme is not a plain object`,
);
});
test(`ThemeProvider and broken function theme`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const incorrectAugment = () => false;
t.throws(
() => {
mount(
<ThemeProvider theme={theme}>
<ThemeProvider theme={incorrectAugment} />
</ThemeProvider>,
);
},
Error,
`ThemeProvider should throw if function theme returns not a plain object`,
);
});
test(`ThemeProvider passes theme`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<Trap.Context intercept={actual} />
</ThemeProvider>,
);
t.deepEqual(actual(), expected, `ThemeProvider should pass a theme`);
});
test(`ThemeProvider passes theme instance`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<Trap.Context intercept={actual} />
</ThemeProvider>,
);
t.is(actual(), expected, `ThemeProvider should pass theme instance, if it is not nested`);
});
test(`ThemeProvider passes theme deep into tree`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<div>
<div>
<Trap.Context intercept={actual} />
</div>
</div>
</ThemeProvider>,
);
t.deepEqual(
actual(),
expected,
`ThemeProvider should pass a theme deep down into tree`,
);
});
test(`ThemeProvider passes theme through PureComponent`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={expected}>
<Pure>
<Trap.Context intercept={actual} />
</Pure>
</ThemeProvider>,
);
t.deepEqual(
actual(),
expected,
`ThemeProvider should pass a theme through PureComponent`,
);
});
test(`ThemeProvider themes objects merging`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const patch = { merged: true };
const actual = getInterceptor();
const expected = { themed: true, merged: true };
mount(
<ThemeProvider theme={theme}>
<ThemeProvider theme={patch}>
<Trap.Context intercept={actual} />
</ThemeProvider>
</ThemeProvider>,
);
// console.log({ actual: actual() });
// t.is(1, 1);
t.deepEqual(actual(), expected, `ThemeProvider should merge themes`);
});
test(`ThemeProvider theme augmenting`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const augment = outerTheme =>
Object.assign({}, outerTheme, { augmented: true });
const actual = getInterceptor();
const expected = { themed: true, augmented: true };
mount(
<ThemeProvider theme={theme}>
<ThemeProvider theme={augment}>
<Trap.Context intercept={actual} />
</ThemeProvider>
</ThemeProvider>,
);
t.deepEqual(actual(), expected, `ThemeProvider should augmented theme`);
});
test(`ThemeProvider propagates theme updates`, t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const update = { updated: true };
const actual = getInterceptor();
const expected = update;
const wrapper = mount(
<ThemeProvider theme={theme}>
<Trap.Context intercept={actual} />
</ThemeProvider>,
);
wrapper.setProps({ theme: expected });
t.deepEqual(actual(), expected, `ThemeProvider should pass theme update`);
});
test('ThemeProvider propagates theme updates even through PureComponent', t => {
const ThemeProvider = createThemeProvider();
const theme = { themed: true };
const update = { updated: true };
const actual = getInterceptor();
const expected = update;
const wrapper = mount(
<ThemeProvider theme={theme}>
<Pure>
<Trap.Context intercept={actual} />
</Pure>
</ThemeProvider>,
);
wrapper.setProps({ theme: expected });
t.deepEqual(
actual(),
expected,
`ThemeProvider should pass theme update even through PureComponent`,
);
});

View File

@@ -0,0 +1,34 @@
import React from 'react';
import channel from './channel';
import createThemeListener from './create-theme-listener';
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component';
export default function createWithTheme(CHANNEL = channel) {
const themeListener = createThemeListener(CHANNEL);
return Component =>
class WithTheme extends React.Component {
static displayName = `WithTheme(${getDisplayName(Component)})`;
static contextTypes = themeListener.contextTypes;
constructor(props, context) {
super(props, context);
this.state = { theme: themeListener.initial(context) };
this.setTheme = theme => this.setState({ theme });
}
componentDidMount() {
this.unsubscribe = themeListener.subscribe(this.context, this.setTheme);
}
componentWillUnmount() {
if (typeof this.unsubscribe === 'function') {
this.unsubscribe();
}
}
render() {
const { theme } = this.state;
return <Component theme={theme} {...this.props} />;
}
};
}

View File

@@ -0,0 +1,237 @@
import test from 'ava';
import React, { Component } from 'react';
import { mount, shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import isFunction from 'is-function';
import createWithTheme from './create-with-theme';
import channel from './channel';
import createBroadcast from 'brcast';
import {
getChannel,
Comp,
Pure,
Trap,
mountOptions,
getInterceptor,
} from './test-helpers';
configure({ adapter: new Adapter() });
test(`createWithTheme's type`, t => {
const actual = isFunction(createWithTheme);
t.true(actual, `createWithTheme should be a function`);
});
test(`createWithTheme's result is function on its own`, t => {
const withTheme = createWithTheme();
const actual = isFunction(withTheme);
t.true(actual, `withTheme should be a function`);
});
test(`withTheme(Comp) result instance type`, t => {
const withTheme = createWithTheme();
const actual = Component.isPrototypeOf(withTheme(Comp));
t.true(actual, `withTheme(Comp) should be a React Component`);
});
test(`withTheme(Comp)'s default channel`, t => {
const withTheme = createWithTheme();
const actual = getChannel(withTheme(Comp));
const expected = channel;
t.is(actual, expected, `withTheme(Comp) should have default channel`);
});
test(`withTheme(Comp) custom channel`, t => {
const custom = '__CUSTOM__';
const withTheme = createWithTheme(custom);
const actual = getChannel(withTheme(Comp));
const expected = custom;
t.is(actual, expected, `createWithTheme() should work with custom channel`);
});
test(`withTheme(Comp) and stateless component`, t => {
const withTheme = createWithTheme();
const StatelessComp = (...props) => <div {...props} />;
const ThemedComp = withTheme(StatelessComp);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const wrapper = shallow(
<div><ThemedComp /></div>,
mountOptions(broadcast),
).childAt(0);
const actual = wrapper.name();
const expected = `WithTheme(StatelessComp)`;
t.is(
actual,
expected,
`withTheme(Comp) should include wrapped stateless component's name in the displayName`,
);
});
test(`withTheme(Comp) and statefull component`, t => {
const withTheme = createWithTheme();
class StatefullComp extends Component {
render() {
return <div {...this.props} />;
}
}
const ThemedComp = withTheme(StatefullComp);
const theme = { themed: true };
const broadcast = createBroadcast(theme);
const wrapper = shallow(
<div><ThemedComp /></div>,
mountOptions(broadcast),
).childAt(0);
const actual = wrapper.name();
const expected = `WithTheme(StatefullComp)`;
t.is(
actual,
expected,
`withTheme(Comp) should include wrapped statefull component's name in the displayName`,
);
});
test(`withTheme(Comp) unsubscribes on unmounting`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(theme);
const unsubscribed = getInterceptor(false);
const wrapper = mount(
<ComponentWithTheme intercept={() => {}} />,
mountOptions(broadcast),
);
wrapper.instance().unsubscribe = () => unsubscribed(true);
t.false(unsubscribed());
wrapper.unmount();
t.true(unsubscribed(), `withTheme(Comp) should unsubscribe on unmounting`);
});
test(`withTheme(Comp) without ThemeProvider`, t => {
const withTheme = createWithTheme();
const ComponentWithTheme = withTheme(Trap.Prop);
t.throws(
() => {
mount(<ComponentWithTheme intercept={() => {}} />);
},
Error,
`withTheme(Comp) should throw if used without appropriate context`,
);
});
test(`withTheme(Comp) receive theme`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(theme);
mount(<ComponentWithTheme intercept={actual} />, mountOptions(broadcast));
t.deepEqual(actual(), expected, `withTheme(Comp) should receive theme`);
});
test(`withTheme(Comp) receive theme deep into tree`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(expected);
mount(
<div>
<div>
<ComponentWithTheme intercept={actual} />
</div>
</div>,
mountOptions(broadcast),
);
t.deepEqual(
actual(),
expected,
`withTheme(Comp) should receive a theme deep down into tree`,
);
});
test(`withTheme(Comp) receives theme through PureComponent`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const actual = getInterceptor();
const expected = theme;
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(expected);
mount(
<Pure>
<ComponentWithTheme intercept={actual} />
</Pure>,
mountOptions(broadcast),
);
t.deepEqual(
actual(),
expected,
`withTheme(Comp) should receive theme through PureComponent`,
);
});
test(`withTheme(Comp) receives theme updates`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const update = { updated: true };
const actual = getInterceptor();
const expected = update;
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(theme);
mount(<ComponentWithTheme intercept={actual} />, mountOptions(broadcast));
broadcast.setState(update);
t.deepEqual(
actual(),
expected,
`withTheme(Comp) should receive theme updates`,
);
});
test(`withTheme(Comp) receives theme updates even through PureComponent`, t => {
const withTheme = createWithTheme();
const theme = { themed: true };
const update = { updated: true };
const actual = getInterceptor();
const expected = update;
const ComponentWithTheme = withTheme(Trap.Prop);
const broadcast = createBroadcast(theme);
mount(
<Pure>
<ComponentWithTheme intercept={actual} />
</Pure>,
mountOptions(broadcast),
);
broadcast.setState(update);
t.deepEqual(
actual(),
expected,
`withTheme(Comp) should receive theme updates even through PureComponent`,
);
});

View File

@@ -0,0 +1,25 @@
import createThemeProvider from './create-theme-provider';
import createWithTheme from './create-with-theme';
import createThemeListener from './create-theme-listener';
import defaultChannel from './channel';
export const channel = defaultChannel;
export const withTheme = createWithTheme();
export const ThemeProvider = createThemeProvider();
export const themeListener = createThemeListener();
export function createTheming(customChannel = defaultChannel) {
return {
channel: customChannel,
withTheme: createWithTheme(customChannel),
ThemeProvider: createThemeProvider(customChannel),
themeListener: createThemeListener(customChannel),
};
}
export default {
channel: defaultChannel,
withTheme,
ThemeProvider,
themeListener,
createTheming,
};

View File

@@ -0,0 +1,169 @@
import test from 'ava';
import React from 'react';
import { mount, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import isFunction from 'is-function';
import isPlainObject from 'is-plain-object';
import { Trap, Pure, Comp, getInterceptor, getChannel } from './test-helpers';
import { channel, createTheming, ThemeProvider, withTheme } from './index';
configure({ adapter: new Adapter() });
test(`createTheming's type`, t => {
const actual = isFunction(createTheming);
t.true(actual, `createTheming should be a function`);
});
test(`createTheming()'s type`, t => {
const theming = createTheming();
const actual = isPlainObject(theming);
t.true(actual, `createTheming() should be an object`);
});
test(`createTheming()'s key names`, t => {
const theming = createTheming();
const actual = Object.keys(theming);
const expected = ['channel', 'withTheme', 'ThemeProvider', 'themeListener'];
t.deepEqual(
actual,
expected,
`createTheming()' keys are withTheme and ThemeProvider`,
);
});
test(`theming default channel`, t => {
const defaultChannel = channel;
const theming = createTheming();
const actual = {
themeProviderChannel: getChannel(theming.ThemeProvider),
withThemeChannel: getChannel(theming.withTheme(Comp)),
};
const expected = {
themeProviderChannel: defaultChannel,
withThemeChannel: defaultChannel,
};
t.deepEqual(
actual,
expected,
`createTheming() hocs have default channel by default`,
);
});
test(`theming custom channel`, t => {
const customChannel = '__CUSTOM__';
const theming = createTheming(customChannel);
const actual = {
themeProviderChannel: getChannel(theming.ThemeProvider),
withThemeChannel: getChannel(theming.withTheme(Comp)),
};
const expected = {
themeProviderChannel: customChannel,
withThemeChannel: customChannel,
};
t.deepEqual(
actual,
expected,
`createTheming() hocs have custom channel if one is provided`,
);
});
test('Theming and initial theme', t => {
const theme = { themed: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<ComponentWithTheme intercept={actual} />
</ThemeProvider>,
);
t.deepEqual(actual(), expected, `Theming passes initial theme`);
});
test('Theming, intitial theme and deep react tree', t => {
const theme = { themed: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<ComponentWithTheme intercept={actual} />
</ThemeProvider>,
);
t.deepEqual(
actual(),
expected,
`Theming should pass initial theme through deep react tree`,
);
});
test('Theming, intitial theme and Pure Component', t => {
const theme = { themed: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const actual = getInterceptor();
const expected = theme;
mount(
<ThemeProvider theme={theme}>
<Pure>
<ComponentWithTheme intercept={actual} />
</Pure>
</ThemeProvider>,
);
t.deepEqual(
actual(),
expected,
`Theming should pass initial theme through PureComponent`,
);
});
test('Theming and updates', t => {
const theme = { themed: true };
const update = { updated: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const actual = getInterceptor();
const expected = update;
const wrapper = mount(
<ThemeProvider theme={theme}>
<ComponentWithTheme intercept={actual} />
</ThemeProvider>,
);
wrapper.setProps({ theme: update });
t.deepEqual(actual(), expected, `default theming should pass theme update`);
});
test('Theming, updates and PureComponent', t => {
const theme = { themed: true };
const update = { updated: true };
const ComponentWithTheme = withTheme(Trap.Prop);
const actual = getInterceptor();
const expected = update;
const wrapper = mount(
<ThemeProvider theme={theme}>
<Pure>
<ComponentWithTheme intercept={actual} />
</Pure>
</ThemeProvider>,
);
wrapper.setProps({ theme: update });
t.deepEqual(
actual(),
expected,
`default theming should pass theme update through Pure Component`,
);
});

View File

@@ -0,0 +1,90 @@
import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import channel from './channel';
export const getContextTypes = C => C.contextTypes;
export const getChannel = C => Object.keys(getContextTypes(C))[0];
export const mountOptions = broadcast => ({
childContextTypes: {
[channel]: PropTypes.object.isRequired,
},
context: {
[channel]: broadcast,
},
});
export function getInterceptor(initialState) {
let state = initialState;
return newState => {
if (newState) {
state = newState;
}
return state;
};
}
export const Comp = props => <div {...props} />;
export class Pure extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
};
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
export class PropTrap extends Component {
static propTypes = {
intercept: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.props.intercept(props.theme);
}
componentWillReceiveProps(nextProps) {
if (nextProps) {
this.props.intercept(nextProps.theme);
}
}
// eslint-disable-next-line
render() {
return <div />;
}
}
export class ContextTrap extends Component {
static propTypes = {
intercept: PropTypes.func.isRequired,
};
static contextTypes = {
[channel]: PropTypes.object.isRequired,
};
constructor(props, context) {
super(props, context)
this.broadcast = this.context[channel];
if (this.broadcast) {
this.props.intercept(this.broadcast.getState());
}
}
componentDidMount() {
if (this.broadcast) {
this.unsubscribe = this.broadcast.subscribe(this.props.intercept);
}
}
// eslint-disable-next-line
render() {
return <div />;
}
}
export const Trap = {
Prop: PropTrap,
Context: ContextTrap,
};