Repository: UniversalAvenue/react-compose
Branch: master
Commit: 3fd02c2dc633
Files: 20
Total size: 62.5 KB
Directory structure:
gitextract_1fv0fe_m/
├── .babelrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── lib/
│ ├── __tests__/
│ │ ├── compose-test.js
│ │ └── connect-test.js
│ ├── config.js
│ ├── connect.js
│ ├── getDisplayName.js
│ └── index.js
├── package.json
└── src/
├── __tests__/
│ ├── .eslintrc.js
│ ├── compose-test.js
│ └── connect-test.js
├── config.js
├── connect.js
├── getDisplayName.js
└── index.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [ "react", "stage-0", "es2015" ],
"plugins": [ "lodash" ],
}
================================================
FILE: .eslintrc.js
================================================
module.exports = {
extends: 'airbnb',
parser: 'babel-eslint',
plugins: [
'react',
],
rules: {
'react/jsx-filename-extension': [1, {
extensions: ['.js', '.jsx'],
}],
},
};
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
*.sw*
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 UniversalAvenue
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

[](http://commitizen.github.io/cz-cli/)
[](https://circleci.com/gh/UniversalAvenue/react-compose/tree/master)
**React-compose** allows you to encapsulate component logic into smaller,
reusable functions, which in turn can be combined back into component. The
fundamental idea is that React components has a way of becoming bloated with,
often repeated, logic. This lib provides you with a set of tools to avoid that.
The encapsulated pieces will be easily testable, either because they are
constant or since their functionality has a more narrow scope than a
corresponding component would have.
The other aspect of **react-compose** is based upon the fact that whenever you
create a React component, you also create an api for it as well. It is
essential, for any large scale project that this api is well formed and consistent
across the application. Most components should also be extendable too, which is
why, significant care is needed to make sure that each component doesn't break
these rules.
Let's show a simple example of extendablity:
```javascript
const ButtonComponent = props => {
const {
onClick,
label,
} = props;
return {label} ;
};
```
Now if a developer would like to manipulate the style of `ButtonComponent` from
the outside, it would have to be changed accordingly:
```javascript
const ButtonComponent = props => {
const {
onClick,
style,
label,
} = props;
return {label} ;
};
```
On the other hand, if all props should be passed down to the `button` element,
the following is much more useful:
```javascript
const ButtonComponent = props => {
const {
label,
} = props;
return {label} ;
};
```
With **react-compose**, the above would be written as:
```javascript
const labelToChildren = ({ label }) => ({ children: label });
const ButtonComponent = compose(labelToChildren)('button');
```
Leaving much less room for breaking the rules of extendability and resuability.
The CustomComponent should essentially work as you would expect that the basic
html elements does, `ButtonComponent` ~ `button`, beyond of course the added
behavior.
As an extra bonus, it is also more straight forward to test the encapsulated
behavior rather than the component as a whole.
```javascript
describe('labelToChildren', () => {
it('should pass whatever input label as children', () => {
expect(labelToChildren({ label: 'string' }).children).toEqual('string');
});
});
```
Finally, the heart of **react-compose**, is finding those elementary patterns
that are present in your application. In this case, we can create a nice higher
order function for the `labelToChildren` logic.
```javascript
const mixProp = (from, to) => props => ({ [to]: props[from] });
const labelToChildren = mixProp('label', 'children');
```
## Installation
Install package, and check that you are using a matching version of React (^0.14)
```bash
npm install -s react-compose
```
## API
Example api usage:
```javascript
import { compose } from 'react-compose';
const constantProper = {
age: 15,
};
const dynamicProper = props => {
return {
children: `The cat is ${props.age} years old`,
};
};
const Cat = compose(constantProper, dynamicProper)('p');
// =>
The cat is 15 years old
;
```
Specialized style composing
```javascript
import { compose, styles } from 'react-compose';
const constantStyle = {
background: 'red',
};
const dynamicStyle = ({ isActive }) => (!isActive && {
display: 'none',
});
const Component = compose(styles(constantStyle, dynamicStyle))('p');
return (props) => {
return Some text ;
};
```
Stacking custom components
```javascript
import { compose } from 'react-compose';
const Cat = props => {
return The cat is {props.age} years old
;
};
const injectAge = {
age: 5,
};
const Composed = compose(injectAge)(Cat);
// => The cat is 5 years old
```
Composing complex children values
```javascript
import { compose, children } from 'react-compose';
const AgeInfo = props => {
return Age: {props.age} years
;
};
const LengthInfo = props => {
return Length: {props.length} cm
;
};
const HeightInfo = props => {
return Height: {props.height} cm
;
};
const Info = compose(children(AgeInfo, LengthInfo, HeightInfo))('div');
const dogData = {
age: 5,
length: 250,
height: 150,
};
const DogInfo = compose(dogData)(Info);
// =>
//
Age: 5
//
Length: 250
//
Height: 150
//
```
Composing classNames, using the awesome [classnames](https://github.com/JedWatson/classnames) lib
```javascript
import { compose, classNames } from 'react-compose';
const btnClassNames = classNames('btn',
({ pressed }) => pressed && 'btn-pressed',
({ hover }) => hover && 'btn-hover');
const Button = compose(btnClassNames)('button');
// pressed: true =>
// pressed: false, hover: true =>
```
================================================
FILE: circle.yml
================================================
machine:
node:
version: 8.0.0
dependencies:
pre:
- npm install -g npm
test:
post:
- npm run semantic-release || true
================================================
FILE: lib/__tests__/compose-test.js
================================================
'use strict';
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
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; }
jest.autoMockOff();
var _ = require('lodash');
var _require = require('enzyme'),
shallow = _require.shallow;
var optimize = require('../index').optimize;
var applyFunctor = require('../index').applyFunctor;
var compose = require('../index').compose;
var styles = require('../index').styles;
var children = require('../index').children;
var classNames = require('../index').classNames;
var mapProp = require('../index').mapProp;
describe('optimize', function () {
it('should merge propers', function () {
var res = optimize({
propA: 'alpha'
}, {
propB: 2
}, function () {
return { width: 400 };
}, function () {
return { width: 400 };
});
expect(res.constant).toEqual({
propA: 'alpha',
propB: 2
});
expect(res.dynamic.length).toEqual(2);
});
it('should merge propers ignore constants after dynamics', function () {
var res = optimize({
propA: 'alpha'
}, function () {
return { width: 400 };
}, { propB: 2 }, function () {
return { width: 400 };
});
expect(res.constant).toEqual({
propA: 'alpha'
});
expect(res.dynamic.length).toEqual(3);
});
});
describe('Apply functor', function () {
var functorCreator = function functorCreator(key, value) {
return function () {
return _defineProperty({}, key, value);
};
};
var deepFunctorCreator = function deepFunctorCreator(values) {
return function () {
return _.map(values, function (value, idx) {
return functorCreator(idx, value);
});
};
};
it('should apply each functor in order', function () {
var functors = [functorCreator('a', 1), functorCreator('b', 3), functorCreator('b', 2)];
var res = _.assign.apply(_, [{}].concat(_toConsumableArray(applyFunctor(functors))));
expect(res).toEqual({
a: 1,
b: 2
});
});
it('should apply deep functors', function () {
var functors = [functorCreator('a', 1), deepFunctorCreator(['alpha', 'beta', 'ceta'])];
var res = _.assign.apply(_, [{}].concat(_toConsumableArray(applyFunctor(functors))));
expect(res).toEqual({
a: 1,
0: 'alpha',
1: 'beta',
2: 'ceta'
});
});
});
var PropTypes = require('prop-types');
var React = require('react');
describe('Compose', function () {
var mapPropToKeyFunctor = function mapPropToKeyFunctor(propKey, key) {
return function (props) {
return _defineProperty({}, key, props[propKey]);
};
};
it('should produce a valid component', function () {
var Compo = compose({ background: 'blue' }, { children: 'boo' })('p');
var wrapper = shallow(React.createElement(Compo, null));
var para = wrapper.find('p');
expect(para.node.props.background).toEqual('blue');
});
it('should pass fed props into style functors', function () {
var Compo = compose({ background: 'blue', strength: '400px' }, mapPropToKeyFunctor('strength', 'fontSize'))('p');
var wrapper = shallow(React.createElement(Compo, { style: { color: 'white' } }));
var para = wrapper.find('p').node;
expect(para.props.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
expect(para.props.fontSize).toEqual('400px');
});
});
describe('Styles', function () {
var pToK = function pToK(propKey, key) {
return function (props) {
return _defineProperty({}, key, props[propKey]);
};
};
it('should produce a valid component', function () {
var Compo = compose(styles({ background: 'blue' }, { color: 'white' }))('p');
var para = shallow(React.createElement(Compo, null)).find('p').node;
expect(para.props.style.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
});
it('should produce a valid component with two separate styles', function () {
var Compo = compose(styles({ background: 'blue' }), styles({ color: 'white' }))('p');
var para = shallow(React.createElement(Compo, null)).find('p').node;
expect(para.props.style.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
});
it('should produce a valid component with two dynamic stylers', function () {
var Compo = compose({ strength: '5px', weight: 'normal' }, styles(pToK('strength', 'fontSize'), pToK('weight', 'fontWeight')))('p');
var para = shallow(React.createElement(Compo, null)).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
expect(para.props.style.fontWeight).toEqual('normal');
});
it('should produce a valid component with composite dynamic stylers', function () {
var fontStyle = {
fontSize: '5px',
fontWeight: 'normal'
};
var colorStyle = {
color: 'blue',
backgroundColor: 'white'
};
var compositeStyle = function compositeStyle() {
return [fontStyle, colorStyle];
};
var Compo = compose(styles(compositeStyle))('p');
var para = shallow(React.createElement(Compo, null)).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
expect(para.props.style.fontWeight).toEqual('normal');
});
it('should produce a valid component with composite multilayer dynamic stylers', function () {
var fontStyle = pToK('strength', 'fontSize');
var colorStyle = {
color: 'blue',
backgroundColor: 'white'
};
var compositeStyle = function compositeStyle() {
return [fontStyle, colorStyle];
};
var Compo = compose({ strength: '5px' }, styles(compositeStyle))('p');
var para = shallow(React.createElement(Compo, null)).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
});
});
describe('Children', function () {
it('should produce a valid component', function () {
var Alpha = function Alpha(props) {
return React.createElement(
'span',
null,
'The cat is ' + props.feeling
);
};
Alpha.propTypes = {
feeling: PropTypes.string.isRequired
};
var Compo = compose({ feeling: 'angry' }, children(Alpha))('p');
var para = shallow(React.createElement(Compo, null)).childAt(0).shallow().node;
expect(para.props.children).toEqual('The cat is angry');
});
});
describe('classNames', function () {
it('should produce a correct className', function () {
var result = classNames('btn', 'btn-pressed')({});
expect(result.className).toEqual('btn btn-pressed');
});
it('should handle classNames propers', function () {
var result = classNames('btn', function (_ref4) {
var pressed = _ref4.pressed;
return pressed && 'btn-pressed';
})({
pressed: true
});
expect(result.className).toEqual('btn btn-pressed');
});
it('should handle falsy classNames propers', function () {
var result = classNames('btn', function (_ref5) {
var pressed = _ref5.pressed;
return pressed && 'btn-pressed';
})({
pressed: false
});
expect(result.className).toEqual('btn');
});
it('should append with input classNames', function () {
var result = classNames('btn', function (_ref6) {
var pressed = _ref6.pressed;
return pressed && 'btn-pressed';
})({
pressed: false,
className: 'alpha'
});
expect(result.className).toEqual('btn alpha');
});
});
describe('Nesting', function () {
it('should optimize nested compose calls', function () {
var Root = function Root(props) {
return React.createElement(
'p',
props,
'root'
);
};
var Level1 = compose({ background: 'red' })(Root);
var Level2 = compose({ color: 'blue' })(Level1);
var wrapper = shallow(React.createElement(Level2, null));
var para = wrapper.shallow().find('p').node;
expect(para.props.background).toEqual('red');
expect(para.props.color).toEqual('blue');
});
fit('should optimize nested compose calls and dynamics should be correct', function () {
var Root = function Root(props) {
return React.createElement(
'p',
props,
'root'
);
};
var Level1 = compose({ background: 'red' }, function () {
return { color: 'red' };
})(Root);
var Level2 = compose({ color: 'blue' }, function (_ref7) {
var background = _ref7.background;
return {
background: background === 'red' ? 'blue' : 'brown'
};
})(Level1);
var wrapper = shallow(React.createElement(Level2, null));
var para = wrapper.shallow().find('p').node;
expect(para.props.background).toEqual('red');
expect(para.props.color).toEqual('red');
});
it('should produce a great display name', function () {
function Root() {
return React.createElement(
'p',
null,
'Names'
);
}
var Level1 = compose({ background: 'red' })(Root);
var Level2 = compose({ color: 'blue' })(Level1);
expect(Level2.displayName).toEqual('composed(Root)');
});
});
describe('mapProp', function () {
it('should transform input value', function () {
function Root(props) {
return React.createElement('p', props);
}
var Comped = compose(mapProp('x', function (x) {
return x * 2;
}))(Root);
var p = shallow(React.createElement(Comped, { x: 5 })).node;
expect(p.props.x).toEqual(10);
});
});
describe('composing', function () {
it('chains properly', function () {
var f0 = {
a: 7
};
function f1() {
return { b: 5 };
}
function f2(_ref8) {
var a = _ref8.a,
b = _ref8.b;
return { c: a + b };
}
var Comped = compose(f0, f1, f2)('p');
var p = shallow(React.createElement(Comped, null)).node;
expect(p.props.c).toEqual(12);
});
it('chains properly while nesting', function () {
var f0 = {
a: 7
};
function f1() {
return { b: 5 };
}
function f2(_ref9) {
var a = _ref9.a,
b = _ref9.b;
return { c: a + b };
}
function f3() {
return { d: 5 };
}
var C1 = compose(f2)('p');
var Comped = compose(f0, f1, f3)(C1);
var p = shallow(React.createElement(Comped, null)).node;
expect(p.props.c).toEqual(12);
});
it('chains properly while deeply nesting', function () {
var f0 = {
a: 7
};
function f1() {
return { b: 5 };
}
function f2(_ref10) {
var a = _ref10.a,
b = _ref10.b;
return { c: a + b };
}
function f3(_ref11) {
var c = _ref11.c;
return { d: c + 2 };
}
var C0 = compose(f3)('p');
var C1 = compose(f2)(C0);
var Comped = compose(f0, f1)(C1);
var p = shallow(React.createElement(Comped, null)).node;
expect(p.props.d).toEqual(14);
});
});
================================================
FILE: lib/__tests__/connect-test.js
================================================
'use strict';
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; };
jest.autoMockOff();
var React = require('react');
var _require = require('enzyme'),
mount = _require.mount;
var thunk = require('redux-thunk').default;
var connect = require('../connect').default;
var applyMiddleware = require('../connect').applyMiddleware;
describe('connect', function () {
it('creates a valid component', function () {
var reducer = jest.fn(function (state) {
return state;
});
var Component = connect(reducer, { onClick: 'doClick' })('button');
var wrapper = mount(React.createElement(Component, null));
wrapper.find('button').simulate('click');
expect(reducer.mock.calls[0][0]).toEqual({});
expect(reducer.mock.calls[1][1].type).toEqual('doClick');
});
it('changes a prop as a result', function () {
var Button = function Button(props) {
return React.createElement('button', props);
};
var reducer = jest.fn(function (state, action) {
if (action.type === 'doClick') {
return _extends({}, state, {
isToggled: !state.isToggled
});
}
return state;
});
var Component = connect(reducer, { onClick: 'doClick' })(Button);
var wrapper = mount(React.createElement(Component, null));
var btn = wrapper.find('button');
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(true);
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(false);
wrapper.setProps({ isToggled: true });
expect(btn.prop('isToggled')).toEqual(true);
});
it('hides willReceiveProps behaviour', function () {
var Button = function Button(props) {
return React.createElement('button', props);
};
var reducer = jest.fn(function (state) {
return state;
});
var Component = connect(reducer, {}, {
componentWillReceiveProps: function componentWillReceiveProps() {}
})(Button);
var wrapper = mount(React.createElement(Component, { isToggled: true }));
var btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
wrapper.setProps({ isToggled: false });
expect(btn.prop('isToggled')).toEqual(true);
});
it('update onDidMount', function () {
var Button = function Button(props) {
return React.createElement('button', props);
};
var reducer = jest.fn(function (state, action) {
if (action.type === 'doClick') {
return _extends({}, state, {
isToggled: !state.isToggled
});
}
return state;
});
var Component = connect(reducer, {}, {
componentDidMount: function componentDidMount() {
this.dispatch({
type: 'doClick'
});
}
})(Button);
var wrapper = mount(React.createElement(Component, null));
var btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
});
it('can handle short form life cycles', function () {
var Button = function Button(props) {
return React.createElement('button', props);
};
var reducer = jest.fn(function (state, action) {
if (action.type === 'doClick') {
return _extends({}, state, {
isToggled: !state.isToggled
});
}
return state;
});
var Component = connect(reducer, {}, {
componentDidMount: 'doClick'
})(Button);
var wrapper = mount(React.createElement(Component, null));
var btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
});
it('works with middleware like redux-thunk', function () {
var Button = function Button(props) {
return React.createElement('button', props);
};
var reducer = jest.fn(function (state, action) {
if (action.type === 'doClick') {
return _extends({}, state, {
isToggled: !state.isToggled
});
}
return state;
});
var ac = applyMiddleware(thunk);
var Component = ac(reducer, function (dispatch) {
return {
onClick: function onClick() {
dispatch(function (disp) {
disp({ type: 'doClick' });
});
}
};
})(Button);
var wrapper = mount(React.createElement(Component, null));
var btn = wrapper.find('button');
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(true);
});
});
================================================
FILE: lib/config.js
================================================
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.plugin = exports.renderChild = exports.composeComponent = exports.exposeContextTypes = undefined;
var _assign2 = require('lodash/assign');
var _assign3 = _interopRequireDefault(_assign2);
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 _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
var plugin = {};
var functions = {};
var configurable = function configurable(key, defaultFn) {
var argGetter = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {
return [];
};
var apply = function apply(fn) {
return function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return fn.apply(null, [].concat(args, _toConsumableArray(argGetter())));
};
};
plugin[key] = function (fn) {
functions[key] = apply(fn);
};
functions[key] = apply(defaultFn);
return function () {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return functions[key].apply(null, args);
};
};
var defaultContextTypes = function defaultContextTypes() {
return {};
};
var exposeContextTypes = exports.exposeContextTypes = configurable('exposeContextTypes', defaultContextTypes, function () {
return [_propTypes2.default];
});
var merge = function merge(arr) {
return _assign3.default.apply(_assign3.default.apply.placeholder, [{}].concat(_toConsumableArray(arr)));
};
var defaultComposeComponent = function defaultComposeComponent(Component, _ref) {
var _ref$styles = _ref.styles,
styles = _ref$styles === undefined ? [] : _ref$styles,
_ref$style = _ref.style,
style = _ref$style === undefined ? {} : _ref$style,
rest = _objectWithoutProperties(_ref, ['styles', 'style']);
var mergedStyle = merge([style].concat(styles));
return _react2.default.createElement(Component, _extends({ style: mergedStyle }, rest));
};
var composeComponent = exports.composeComponent = configurable('composeComponent', defaultComposeComponent);
var defaultRenderChild = function defaultRenderChild(props) {
return function (Child, index) {
return _react2.default.createElement(Child, _extends({}, props, { key: (Child.displayName || '') + index }));
};
};
var renderChild = exports.renderChild = configurable('renderChild', defaultRenderChild);
exports.plugin = plugin;
================================================
FILE: lib/connect.js
================================================
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _each2 = require('lodash/each');
var _each3 = _interopRequireDefault(_each2);
var _omitBy2 = require('lodash/omitBy');
var _omitBy3 = _interopRequireDefault(_omitBy2);
var _reduce2 = require('lodash/reduce');
var _reduce3 = _interopRequireDefault(_reduce2);
var _isString2 = require('lodash/isString');
var _isString3 = _interopRequireDefault(_isString2);
var _isFunction2 = require('lodash/isFunction');
var _isFunction3 = _interopRequireDefault(_isFunction2);
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; }; }();
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; };
exports.default = connect;
exports.applyMiddleware = applyMiddleware;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _getDisplayName = require('./getDisplayName');
var _getDisplayName2 = _interopRequireDefault(_getDisplayName);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
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; }
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; }
var defaultMergeProps = function defaultMergeProps(stateProps, dispatchProps, parentProps) {
return _extends({}, parentProps, stateProps, dispatchProps);
};
function createDispatchProps(dispatchers, dispatch) {
if ((0, _isFunction3.default)(dispatchers)) {
return dispatchers(dispatch);
}
function dispatchHandler(fn) {
var action = (0, _isString3.default)(fn) ? { type: fn } : fn;
return function () {
return dispatch(action);
};
}
return (0, _reduce3.default)(dispatchers, function (sum, fn, key) {
return Object.assign(sum, _defineProperty({}, key, dispatchHandler(fn)));
}, {});
}
function wrapLifeCycle(fn) {
if (!(0, _isFunction3.default)(fn)) {
var action = (0, _isString3.default)(fn) ? { type: fn } : fn;
return function wrappedStaticLifeCycleMethod() {
this.dispatch(action);
};
}
return fn;
}
function connect(reducer, dispatchers) {
var lifeCycle = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var merge = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : defaultMergeProps;
var middlewares = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : [];
return function (Component) {
var Connected = function (_React$Component) {
_inherits(Connected, _React$Component);
function Connected(props) {
_classCallCheck(this, Connected);
var _this = _possibleConstructorReturn(this, (Connected.__proto__ || Object.getPrototypeOf(Connected)).call(this, props));
_this.state = reducer(props, {});
_this.dispatch = _this.dispatch.bind(_this);
var middlewareAPI = {
getState: function getState() {
return _this.state;
},
dispatch: _this.dispatch
};
var chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
_this.dispatch = [].concat(_toConsumableArray(chain)).reduceRight(function (a, fn) {
return fn(a);
}, _this.dispatch);
_this.dispatchProps = createDispatchProps(dispatchers, _this.dispatch);
return _this;
}
_createClass(Connected, [{
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var _this2 = this;
this.setState((0, _omitBy3.default)(nextProps, function (val, key) {
return val === _this2.state[key];
}));
}
}, {
key: 'getProps',
value: function getProps() {
return merge(this.state, this.dispatchProps, this.props);
}
}, {
key: 'dispatch',
value: function dispatch(action) {
var nextState = reducer(this.state, action);
if (nextState !== this.state) {
this.setState(nextState);
}
return action;
}
}, {
key: 'render',
value: function render() {
return _react2.default.createElement(Component, this.getProps());
}
}]);
return Connected;
}(_react2.default.Component);
Connected.displayName = 'connect(' + (0, _getDisplayName2.default)(Component) + ')';
(0, _each3.default)(lifeCycle, function (fn, key) {
return Object.assign(Connected.prototype, _defineProperty({}, key, wrapLifeCycle(fn)));
});
return Connected;
};
}
function applyMiddleware() {
for (var _len = arguments.length, wares = Array(_len), _key = 0; _key < _len; _key++) {
wares[_key] = arguments[_key];
}
return function () {
return connect.apply(null, [arguments.length <= 0 ? undefined : arguments[0], arguments.length <= 1 ? undefined : arguments[1], (arguments.length <= 2 ? undefined : arguments[2]) || {}, (arguments.length <= 3 ? undefined : arguments[3]) || defaultMergeProps, wares]);
};
}
================================================
FILE: lib/getDisplayName.js
================================================
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getDisplayName;
// https://github.com/jurassix/react-display-name/blob/master/src/getDisplayName.js
function getDisplayName(Component) {
return Component.displayName || Component.name || (typeof Component === 'string' ? Component : 'Component');
}
================================================
FILE: lib/index.js
================================================
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.children = exports.styles = exports.compose = exports.applyFunctor = exports.optimize = undefined;
var _reduce2 = require('lodash/reduce');
var _reduce3 = _interopRequireDefault(_reduce2);
var _isString2 = require('lodash/isString');
var _isString3 = _interopRequireDefault(_isString2);
var _isArray2 = require('lodash/isArray');
var _isArray3 = _interopRequireDefault(_isArray2);
var _slice2 = require('lodash/slice');
var _slice3 = _interopRequireDefault(_slice2);
var _isFunction2 = require('lodash/isFunction');
var _isFunction3 = _interopRequireDefault(_isFunction2);
var _takeWhile2 = require('lodash/takeWhile');
var _takeWhile3 = _interopRequireDefault(_takeWhile2);
var _flattenDeep2 = require('lodash/flattenDeep');
var _flattenDeep3 = _interopRequireDefault(_flattenDeep2);
var _map2 = require('lodash/map');
var _map3 = _interopRequireDefault(_map2);
var _compact2 = require('lodash/compact');
var _compact3 = _interopRequireDefault(_compact2);
var _flatten2 = require('lodash/flatten');
var _flatten3 = _interopRequireDefault(_flatten2);
var _assign2 = require('lodash/assign');
var _assign3 = _interopRequireDefault(_assign2);
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; };
exports.classNames = classNames;
exports.mapProp = mapProp;
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _config = require('./config');
var _getDisplayName = require('./getDisplayName');
var _getDisplayName2 = _interopRequireDefault(_getDisplayName);
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 _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
/**
* aggregates a set of functions/objects into a constant part + a dynamic part
*
*
**/
var mergeObjArr = function mergeObjArr(arr) {
var base = _assign3.default.apply(_assign3.default.apply.placeholder, [{}].concat(_toConsumableArray(arr)));
var styles = (0, _flatten3.default)((0, _compact3.default)((0, _map3.default)(arr, 'styles')));
if (styles.length > 1) {
base.styles = styles;
}
return base;
};
var optimize = exports.optimize = function optimize() {
for (var _len = arguments.length, propers = Array(_len), _key = 0; _key < _len; _key++) {
propers[_key] = arguments[_key];
}
var flattened = (0, _compact3.default)((0, _flattenDeep3.default)(propers));
var constantStyles = (0, _takeWhile3.default)(flattened, function (st) {
return !(0, _isFunction3.default)(st);
}) || [];
var dynamic = (0, _slice3.default)(flattened, constantStyles.length, flattened.length) || [];
var constant = mergeObjArr(constantStyles);
return {
constant: constant,
dynamic: dynamic
};
};
var flatMap = function flatMap(arr, fn) {
if ((0, _isArray3.default)(arr)) {
return (0, _flattenDeep3.default)((0, _map3.default)(arr, fn));
}
return fn(arr);
};
var applyFunctor = exports.applyFunctor = function applyFunctor(functor) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
var reapply = function reapply(f) {
return applyFunctor.apply(null, [f].concat(args));
};
if ((0, _isArray3.default)(functor)) {
return flatMap(functor, reapply);
}
if ((0, _isFunction3.default)(functor)) {
return reapply(functor.apply(undefined, args));
}
return functor;
};
function mergePropers(a, b) {
return optimize([a.constant].concat(_toConsumableArray(a.dynamic), [b.constant], _toConsumableArray(b.dynamic)));
}
var compose = exports.compose = function compose() {
for (var _len3 = arguments.length, propers = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
propers[_key3] = arguments[_key3];
}
var optimizedPropers = optimize(propers);
function doCompose(Component, ps) {
var ComposedComponent = function ComposedComponent(props, context) {
var base = _extends({}, props, ps.constant);
var finalProps = ps.dynamic.reduce(function (obj, fn) {
return Object.assign({}, obj, applyFunctor(fn, obj, context));
}, base);
return (0, _config.composeComponent)(Component, finalProps);
};
ComposedComponent.contextTypes = (0, _config.exposeContextTypes)();
ComposedComponent.displayName = 'composed(' + (0, _getDisplayName2.default)(Component) + ')';
return ComposedComponent;
}
function mergeComposed(Component) {
var Target = Component;
var ps = optimizedPropers;
if (Component && Component.composedBy) {
var _Component$composedBy = Component.composedBy,
composers = _Component$composedBy.composers,
Parent = _Component$composedBy.Parent;
ps = mergePropers(optimizedPropers, composers);
Target = Parent;
}
var Result = doCompose(Target, ps);
Result.composedBy = {
composers: ps,
Parent: Target
};
return Result;
}
return mergeComposed;
};
var styles = exports.styles = function styles() {
for (var _len4 = arguments.length, stylers = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
stylers[_key4] = arguments[_key4];
}
var _optimize = optimize(stylers),
constant = _optimize.constant,
dynamic = _optimize.dynamic;
// If all constants, return a constant proper
if (dynamic.length === 0) {
return {
styles: constant
};
}
return function (props, context) {
var upstream = props.styles || [];
var base = [].concat(_toConsumableArray(upstream), [constant]);
var applied = applyFunctor(dynamic, _extends({}, props, { styles: base }), context);
var finalStyles = [].concat(_toConsumableArray(base), _toConsumableArray(applied));
return {
styles: finalStyles
};
};
};
var children = exports.children = function children() {
for (var _len5 = arguments.length, childers = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
childers[_key5] = arguments[_key5];
}
return function (props) {
return {
children: (0, _map3.default)(childers, (0, _config.renderChild)(props))
};
};
};
function handleUpstreamClassName(name) {
if (!name) {
return {};
}
if ((0, _isString3.default)(name)) {
return _defineProperty({}, name, true);
}
return name;
}
function arrayToClassNames(arr) {
return (0, _reduce3.default)(arr, function (sum, item) {
return (0, _isString3.default)(item) ? _extends({}, sum, _defineProperty({}, item, true)) : _extends({}, sum, item);
}, {});
}
function classNames() {
for (var _len6 = arguments.length, names = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
names[_key6] = arguments[_key6];
}
return function (props, context) {
return {
className: (0, _classnames2.default)(_extends({}, arrayToClassNames(applyFunctor(names, props, context)), handleUpstreamClassName(props.className)))
};
};
}
function mapProp(property, fn, defaultValue) {
return function (props) {
return _defineProperty({}, property, fn(props[property] || defaultValue));
};
}
================================================
FILE: package.json
================================================
{
"name": "react-compose",
"description": "Compose react components with a functional api",
"main": "lib/index.js",
"scripts": {
"clean": "./node_modules/rimraf/bin.js lib",
"test": "npm run build && ./node_modules/jest-cli/bin/jest.js",
"build": "./node_modules/babel-cli/bin/babel.js src --out-dir lib",
"pkgfiles": "./node_modules/pkgfiles/bin/pkgfiles.js",
"prepublish": "npm run clean && npm run build",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
},
"files": [
"lib",
"!lib/__tests__"
],
"release": {
"verifyConditions": "condition-circle"
},
"repository": {
"type": "git",
"url": "https://github.com/UniversalAvenue/react-compose.git"
},
"keywords": [
"React",
"compose"
],
"author": "Daniel Werthén (https://github.com/danielwerthen)",
"license": "MIT",
"bugs": {
"url": "https://github.com/UniversalAvenue/react-compose/issues"
},
"homepage": "https://github.com/UniversalAvenue/react-compose#readme",
"peerDependencies": {
"react": ">=15.5.4"
},
"devDependencies": {
"babel-cli": "^6.6.5",
"babel-eslint": "^7.2.3",
"babel-plugin-lodash": "^3.2.11",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"condition-circle": "^1.2.0",
"cz-conventional-changelog": "^2.0.0",
"enzyme": "^2.3.0",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^15.0.1",
"eslint-plugin-import": "^2.3.0",
"eslint-plugin-jsx-a11y": "^5.0.3",
"eslint-plugin-react": "^7.0.1",
"jest-cli": "^20.0.4",
"pkgfiles": "^2.3.0",
"react": ">=15.5.4",
"react-addons-test-utils": "^15.5.1",
"react-dom": "^15.5.4",
"redux-thunk": "^2.1.0",
"rimraf": "^2.4.4",
"semantic-release": "^6.3.6"
},
"jest": {
"roots": [
"lib"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"dependencies": {
"classnames": "^2.2.3",
"lodash": "^4.0.0",
"prop-types": "^15.5.10"
}
}
================================================
FILE: src/__tests__/.eslintrc.js
================================================
module.exports = {
env: {
jest: true,
},
};
================================================
FILE: src/__tests__/compose-test.js
================================================
jest.autoMockOff();
const _ = require('lodash');
const { shallow } = require('enzyme');
const optimize = require('../index').optimize;
const applyFunctor = require('../index').applyFunctor;
const compose = require('../index').compose;
const styles = require('../index').styles;
const children = require('../index').children;
const classNames = require('../index').classNames;
const mapProp = require('../index').mapProp;
describe('optimize', () => {
it('should merge propers', () => {
const res = optimize({
propA: 'alpha',
}, {
propB: 2,
}, () => ({ width: 400 }),
() => ({ width: 400 }),
);
expect(res.constant).toEqual({
propA: 'alpha',
propB: 2,
});
expect(res.dynamic.length).toEqual(2);
});
it('should merge propers ignore constants after dynamics', () => {
const res = optimize({
propA: 'alpha',
}, () => ({ width: 400 }),
{ propB: 2 },
() => ({ width: 400 }),
);
expect(res.constant).toEqual({
propA: 'alpha',
});
expect(res.dynamic.length).toEqual(3);
});
});
describe('Apply functor', () => {
const functorCreator = (key, value) => () => ({
[key]: value,
});
const deepFunctorCreator = values => () =>
_.map(values, (value, idx) => functorCreator(idx, value));
it('should apply each functor in order', () => {
const functors = [
functorCreator('a', 1),
functorCreator('b', 3),
functorCreator('b', 2),
];
const res = _.assign.apply(_, [{}, ...applyFunctor(functors)]);
expect(res).toEqual({
a: 1,
b: 2,
});
});
it('should apply deep functors', () => {
const functors = [
functorCreator('a', 1),
deepFunctorCreator(['alpha', 'beta', 'ceta']),
];
const res = _.assign.apply(_, [{}, ...applyFunctor(functors)]);
expect(res).toEqual({
a: 1,
0: 'alpha',
1: 'beta',
2: 'ceta',
});
});
});
const PropTypes = require('prop-types');
const React = require('react');
describe('Compose', () => {
const mapPropToKeyFunctor = (propKey, key) => props => ({
[key]: props[propKey],
});
it('should produce a valid component', () => {
const Compo = compose({ background: 'blue' }, { children: 'boo' })('p');
const wrapper = shallow( );
const para = wrapper.find('p');
expect(para.node.props.background).toEqual('blue');
});
it('should pass fed props into style functors', () => {
const Compo = compose({ background: 'blue', strength: '400px' },
mapPropToKeyFunctor('strength', 'fontSize'))('p');
const wrapper = shallow( );
const para = wrapper.find('p').node;
expect(para.props.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
expect(para.props.fontSize).toEqual('400px');
});
});
describe('Styles', () => {
const pToK = (propKey, key) => props => ({
[key]: props[propKey],
});
it('should produce a valid component', () => {
const Compo = compose(styles({ background: 'blue' }, { color: 'white' }))('p');
const para = shallow( ).find('p').node;
expect(para.props.style.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
});
it('should produce a valid component with two separate styles', () => {
const Compo = compose(styles({ background: 'blue' }), styles({ color: 'white' }))('p');
const para = shallow( ).find('p').node;
expect(para.props.style.background).toEqual('blue');
expect(para.props.style.color).toEqual('white');
});
it('should produce a valid component with two dynamic stylers', () => {
const Compo = compose({ strength: '5px', weight: 'normal' },
styles(pToK('strength', 'fontSize'), pToK('weight', 'fontWeight')),
)('p');
const para = shallow( ).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
expect(para.props.style.fontWeight).toEqual('normal');
});
it('should produce a valid component with composite dynamic stylers', () => {
const fontStyle = {
fontSize: '5px',
fontWeight: 'normal',
};
const colorStyle = {
color: 'blue',
backgroundColor: 'white',
};
const compositeStyle = () => [fontStyle, colorStyle];
const Compo = compose(
styles(compositeStyle),
)('p');
const para = shallow( ).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
expect(para.props.style.fontWeight).toEqual('normal');
});
it('should produce a valid component with composite multilayer dynamic stylers', () => {
const fontStyle = pToK('strength', 'fontSize');
const colorStyle = {
color: 'blue',
backgroundColor: 'white',
};
const compositeStyle = () => [fontStyle, colorStyle];
const Compo = compose({ strength: '5px' },
styles(compositeStyle),
)('p');
const para = shallow( ).find('p').node;
expect(para.props.style.fontSize).toEqual('5px');
});
});
describe('Children', () => {
it('should produce a valid component', () => {
const Alpha = props => {`The cat is ${props.feeling}`} ;
Alpha.propTypes = {
feeling: PropTypes.string.isRequired,
};
const Compo = compose({ feeling: 'angry' }, children(Alpha))('p');
const para = shallow( ).childAt(0).shallow().node;
expect(para.props.children).toEqual('The cat is angry');
});
});
describe('classNames', () => {
it('should produce a correct className', () => {
const result = classNames('btn', 'btn-pressed')({});
expect(result.className).toEqual('btn btn-pressed');
});
it('should handle classNames propers', () => {
const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
pressed: true,
});
expect(result.className).toEqual('btn btn-pressed');
});
it('should handle falsy classNames propers', () => {
const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
pressed: false,
});
expect(result.className).toEqual('btn');
});
it('should append with input classNames', () => {
const result = classNames('btn', ({ pressed }) => pressed && 'btn-pressed')({
pressed: false,
className: 'alpha',
});
expect(result.className).toEqual('btn alpha');
});
});
describe('Nesting', () => {
it('should optimize nested compose calls', () => {
const Root = props => root
;
const Level1 = compose({ background: 'red' })(Root);
const Level2 = compose({ color: 'blue' })(Level1);
const wrapper = shallow( );
const para = wrapper.shallow().find('p').node;
expect(para.props.background).toEqual('red');
expect(para.props.color).toEqual('blue');
});
fit('should optimize nested compose calls and dynamics should be correct', () => {
const Root = props => root
;
const Level1 = compose({ background: 'red' }, () => ({ color: 'red' }))(Root);
const Level2 = compose({ color: 'blue' }, ({ background }) =>
({
background: background === 'red' ? 'blue' : 'brown',
}),
)(Level1);
const wrapper = shallow( );
const para = wrapper.shallow().find('p').node;
expect(para.props.background).toEqual('red');
expect(para.props.color).toEqual('red');
});
it('should produce a great display name', () => {
function Root() {
return Names
;
}
const Level1 = compose({ background: 'red' })(Root);
const Level2 = compose({ color: 'blue' })(Level1);
expect(Level2.displayName).toEqual('composed(Root)');
});
});
describe('mapProp', () => {
it('should transform input value', () => {
function Root(props) {
return
;
}
const Comped = compose(mapProp('x', x => x * 2))(Root);
const p = shallow( ).node;
expect(p.props.x).toEqual(10);
});
});
describe('composing', () => {
it('chains properly', () => {
const f0 = {
a: 7,
};
function f1() {
return { b: 5 };
}
function f2({ a, b }) {
return { c: a + b };
}
const Comped = compose(f0, f1, f2)('p');
const p = shallow( ).node;
expect(p.props.c).toEqual(12);
});
it('chains properly while nesting', () => {
const f0 = {
a: 7,
};
function f1() {
return { b: 5 };
}
function f2({ a, b }) {
return { c: a + b };
}
function f3() {
return { d: 5 };
}
const C1 = compose(f2)('p');
const Comped = compose(f0, f1, f3)(C1);
const p = shallow( ).node;
expect(p.props.c).toEqual(12);
});
it('chains properly while deeply nesting', () => {
const f0 = {
a: 7,
};
function f1() {
return { b: 5 };
}
function f2({ a, b }) {
return { c: a + b };
}
function f3({ c }) {
return { d: c + 2 };
}
const C0 = compose(f3)('p');
const C1 = compose(f2)(C0);
const Comped = compose(f0, f1)(C1);
const p = shallow( ).node;
expect(p.props.d).toEqual(14);
});
});
================================================
FILE: src/__tests__/connect-test.js
================================================
jest.autoMockOff();
const React = require('react');
const { mount } = require('enzyme');
const thunk = require('redux-thunk').default;
const connect = require('../connect').default;
const applyMiddleware = require('../connect').applyMiddleware;
describe('connect', () => {
it('creates a valid component', () => {
const reducer = jest.fn(state => state);
const Component = connect(reducer, { onClick: 'doClick' })('button');
const wrapper = mount( );
wrapper.find('button').simulate('click');
expect(reducer.mock.calls[0][0]).toEqual({});
expect(reducer.mock.calls[1][1].type).toEqual('doClick');
});
it('changes a prop as a result', () => {
const Button = props => ;
const reducer = jest.fn((state, action) => {
if (action.type === 'doClick') {
return {
...state,
isToggled: !state.isToggled,
};
}
return state;
});
const Component = connect(reducer, { onClick: 'doClick' })(Button);
const wrapper = mount( );
const btn = wrapper.find('button');
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(true);
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(false);
wrapper.setProps({ isToggled: true });
expect(btn.prop('isToggled')).toEqual(true);
});
it('hides willReceiveProps behaviour', () => {
const Button = props => ;
const reducer = jest.fn(state => state);
const Component = connect(reducer, {}, {
componentWillReceiveProps() {},
})(Button);
const wrapper = mount( );
const btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
wrapper.setProps({ isToggled: false });
expect(btn.prop('isToggled')).toEqual(true);
});
it('update onDidMount', () => {
const Button = props => ;
const reducer = jest.fn((state, action) => {
if (action.type === 'doClick') {
return {
...state,
isToggled: !state.isToggled,
};
}
return state;
});
const Component = connect(reducer, {}, {
componentDidMount() {
this.dispatch({
type: 'doClick',
});
},
})(Button);
const wrapper = mount( );
const btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
});
it('can handle short form life cycles', () => {
const Button = props => ;
const reducer = jest.fn((state, action) => {
if (action.type === 'doClick') {
return {
...state,
isToggled: !state.isToggled,
};
}
return state;
});
const Component = connect(reducer, {}, {
componentDidMount: 'doClick',
})(Button);
const wrapper = mount( );
const btn = wrapper.find('button');
expect(btn.prop('isToggled')).toEqual(true);
});
it('works with middleware like redux-thunk', () => {
const Button = props => ;
const reducer = jest.fn((state, action) => {
if (action.type === 'doClick') {
return {
...state,
isToggled: !state.isToggled,
};
}
return state;
});
const ac = applyMiddleware(thunk);
const Component = ac(reducer, dispatch => ({
onClick: () => {
dispatch((disp) => {
disp({ type: 'doClick' });
});
},
}))(Button);
const wrapper = mount( );
const btn = wrapper.find('button');
btn.simulate('click');
expect(btn.prop('isToggled')).toEqual(true);
});
});
================================================
FILE: src/config.js
================================================
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
const plugin = {};
const functions = {};
const configurable = (key, defaultFn, argGetter = () => []) => {
const apply = fn => (...args) => fn.apply(null, [...args, ...argGetter()]);
plugin[key] = (fn) => {
functions[key] = apply(fn);
};
functions[key] = apply(defaultFn);
return (...args) => functions[key].apply(null, args);
};
const defaultContextTypes = () => ({});
export const exposeContextTypes = configurable('exposeContextTypes',
defaultContextTypes,
() => [PropTypes]);
const merge = arr => _.assign.apply(_, [{}, ...arr]);
const defaultComposeComponent = (Component, {
styles = [],
style = {},
...rest
}) => {
const mergedStyle = merge([style].concat(styles));
return ;
};
export const composeComponent = configurable('composeComponent', defaultComposeComponent);
const defaultRenderChild = props => (Child, index) =>
;
export const renderChild = configurable('renderChild', defaultRenderChild);
export {
plugin,
};
================================================
FILE: src/connect.js
================================================
import React from 'react';
import _ from 'lodash';
import getDisplayName from './getDisplayName';
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps,
});
function createDispatchProps(dispatchers, dispatch) {
if (_.isFunction(dispatchers)) {
return dispatchers(dispatch);
}
function dispatchHandler(fn) {
const action = _.isString(fn) ? { type: fn } : fn;
return () => dispatch(action);
}
return _.reduce(dispatchers, (sum, fn, key) =>
Object.assign(sum, {
[key]: dispatchHandler(fn),
}),
{});
}
function wrapLifeCycle(fn) {
if (!_.isFunction(fn)) {
const action = _.isString(fn) ? { type: fn } : fn;
return function wrappedStaticLifeCycleMethod() {
this.dispatch(action);
};
}
return fn;
}
export default function connect(reducer,
dispatchers,
lifeCycle = {},
merge = defaultMergeProps,
middlewares = []) {
return (Component) => {
class Connected extends React.Component {
constructor(props) {
super(props);
this.state = reducer(props, {});
this.dispatch = this.dispatch.bind(this);
const middlewareAPI = {
getState: () => this.state,
dispatch: this.dispatch,
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
this.dispatch = [...chain].reduceRight((a, fn) => fn(a), this.dispatch);
this.dispatchProps = createDispatchProps(dispatchers, this.dispatch);
}
componentWillReceiveProps(nextProps) {
this.setState(_.omitBy(nextProps, (val, key) =>
val === this.state[key]));
}
getProps() {
return merge(this.state, this.dispatchProps, this.props);
}
dispatch(action) {
const nextState = reducer(this.state, action);
if (nextState !== this.state) {
this.setState(nextState);
}
return action;
}
render() {
return ;
}
}
Connected.displayName = `connect(${getDisplayName(Component)})`;
_.each(lifeCycle, (fn, key) => Object.assign(Connected.prototype, {
[key]: wrapLifeCycle(fn),
}));
return Connected;
};
}
export function applyMiddleware(...wares) {
return (...args) => connect.apply(null,
[args[0], args[1], args[2] || {}, args[3] || defaultMergeProps, wares]);
}
================================================
FILE: src/getDisplayName.js
================================================
// https://github.com/jurassix/react-display-name/blob/master/src/getDisplayName.js
export default function getDisplayName(Component) {
return (
Component.displayName ||
Component.name ||
(typeof Component === 'string' ? Component : 'Component')
);
}
================================================
FILE: src/index.js
================================================
import _ from 'lodash';
import combineNames from 'classnames';
import { composeComponent, exposeContextTypes, renderChild } from './config';
import getDisplayName from './getDisplayName';
/**
* aggregates a set of functions/objects into a constant part + a dynamic part
*
*
**/
const mergeObjArr = (arr) => {
const base = _.assign.apply(_, [{}, ...arr]);
const styles = _.flatten(_.compact(_.map(arr, 'styles')));
if (styles.length > 1) {
base.styles = styles;
}
return base;
};
export const optimize = (...propers) => {
const flattened = _.compact(_.flattenDeep(propers));
const constantStyles = _.takeWhile(flattened, st => !_.isFunction(st)) || [];
const dynamic = _.slice(flattened, constantStyles.length, flattened.length) || [];
const constant = mergeObjArr(constantStyles);
return {
constant,
dynamic,
};
};
const flatMap = (arr, fn) => {
if (_.isArray(arr)) {
return _.flattenDeep(_.map(arr, fn));
}
return fn(arr);
};
export const applyFunctor = (functor, ...args) => {
const reapply = f => applyFunctor.apply(null, [f, ...args]);
if (_.isArray(functor)) {
return flatMap(functor, reapply);
}
if (_.isFunction(functor)) {
return reapply(functor(...args));
}
return functor;
};
function mergePropers(a, b) {
return optimize([a.constant, ...a.dynamic, b.constant, ...b.dynamic]);
}
export const compose = (...propers) => {
const optimizedPropers = optimize(propers);
function doCompose(Component, ps) {
const ComposedComponent = (props, context) => {
const base = { ...props, ...ps.constant };
const finalProps = ps.dynamic.reduce((obj, fn) =>
Object.assign({}, obj, applyFunctor(fn, obj, context)),
base);
return composeComponent(Component, finalProps);
};
ComposedComponent.contextTypes = exposeContextTypes();
ComposedComponent.displayName = `composed(${getDisplayName(Component)})`;
return ComposedComponent;
}
function mergeComposed(Component) {
let Target = Component;
let ps = optimizedPropers;
if (Component && Component.composedBy) {
const {
composers,
Parent,
} = Component.composedBy;
ps = mergePropers(optimizedPropers, composers);
Target = Parent;
}
const Result = doCompose(Target, ps);
Result.composedBy = {
composers: ps,
Parent: Target,
};
return Result;
}
return mergeComposed;
};
export const styles = (...stylers) => {
const {
constant,
dynamic,
} = optimize(stylers);
// If all constants, return a constant proper
if (dynamic.length === 0) {
return {
styles: constant,
};
}
return (props, context) => {
const upstream = props.styles || [];
const base = [...upstream, constant];
const applied = applyFunctor(dynamic, { ...props, styles: base }, context);
const finalStyles = [...base, ...applied];
return {
styles: finalStyles,
};
};
};
export const children = (...childers) =>
props =>
({
children: _.map(childers, renderChild(props)),
});
function handleUpstreamClassName(name) {
if (!name) {
return {};
}
if (_.isString(name)) {
return {
[name]: true,
};
}
return name;
}
function arrayToClassNames(arr) {
return _.reduce(arr, (sum, item) =>
(_.isString(item) ?
{ ...sum, [item]: true } :
{ ...sum, ...item }),
{});
}
export function classNames(...names) {
return (props, context) => ({
className: combineNames({
...arrayToClassNames(applyFunctor(names, props, context)),
...handleUpstreamClassName(props.className),
}),
});
}
export function mapProp(property, fn, defaultValue) {
return props => ({ [property]: fn(props[property] || defaultValue) });
}