Skip to content
This repository has been archived by the owner on Jun 4, 2020. It is now read-only.

Commit

Permalink
feat(api): Add state composing tool connect
Browse files Browse the repository at this point in the history
Basically it is redux for single components
  • Loading branch information
danielwerthen committed Jun 10, 2016
1 parent a315c2d commit a2cfd88
Show file tree
Hide file tree
Showing 10 changed files with 535 additions and 15 deletions.
1 change: 0 additions & 1 deletion lib/__tests__/compose-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ describe('Children', function () {
};
var Compo = compose({ feeling: 'angry' }, children(Alpha))('p');
var para = shallow(React.createElement(Compo, null)).childAt(0).shallow().node;
console.log(para);
expect(para.props.children).toEqual('The cat is angry');
});
});
Expand Down
136 changes: 136 additions & 0 deletions lib/__tests__/connect-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'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');

var 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);
});
});
172 changes: 172 additions & 0 deletions lib/connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'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 _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

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 _ret = function () {
var action = (0, _isString3.default)(fn) ? { type: fn } : fn;
return {
v: function wrappedStaticLifeCycleMethod() {
this.dispatch(action);
}
};
}();

if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
}
return fn;
}

function connect(reducer, dispatchers) {
var lifeCycle = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var merge = arguments.length <= 3 || arguments[3] === undefined ? defaultMergeProps : arguments[3];
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, 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]);
};
}
10 changes: 10 additions & 0 deletions lib/getDisplayName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'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');
}
11 changes: 5 additions & 6 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ 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; }
Expand Down Expand Up @@ -126,11 +130,6 @@ function mergePropers(a, b) {
return optimize([a.constant].concat(_toConsumableArray(a.dynamic), [b.constant], _toConsumableArray(b.dynamic)));
}

// https://github.com/jurassix/react-display-name/blob/master/src/getDisplayName.js
var getDisplayName = function getDisplayName(Component) {
return Component.displayName || Component.name || (typeof Component === 'string' ? Component : 'Component');
};

var compose = exports.compose = function compose() {
for (var _len3 = arguments.length, propers = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
propers[_key3] = arguments[_key3];
Expand All @@ -145,7 +144,7 @@ var compose = exports.compose = function compose() {
return (0, _config.composeComponent)(Component, finalProps);
};
ComposedComponent.contextTypes = (0, _config.exposeContextTypes)();
ComposedComponent.displayName = 'composed(' + getDisplayName(Component) + ')';
ComposedComponent.displayName = 'composed(' + (0, _getDisplayName2.default)(Component) + ')';
return ComposedComponent;
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@
"eslint-plugin-import": "^1.8.1",
"eslint-plugin-jsx-a11y": "^1.3.0",
"eslint-plugin-react": "^5.1.1",
"jest-cli": "^0.8.1",
"jest-cli": "^12.1.1",
"pkgfiles": "^2.3.0",
"react": ">=0.14.0",
"react-addons-test-utils": ">=0.14.0",
"react-dom": "^15.1.0",
"redux-thunk": "^2.1.0",
"rimraf": "^2.4.4",
"semantic-release": "^4.3.5"
},
Expand Down
Loading

0 comments on commit a2cfd88

Please sign in to comment.