@xailabs/altx
Version:
Flux flavor based on alt.js
268 lines (236 loc) • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.connectAlternative = undefined;
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; }; }();
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
exports.default = connect;
var _connectToStores = require('alt-utils/lib/connectToStores');
var _connectToStores2 = _interopRequireDefault(_connectToStores);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
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; }
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; }
// TODO: deprecate connectAlternative asap!
/* eslint-disable */
/**
* A component decorator for connecting to immutable stores.
*
* Basically a wrapper around `alt/utils/connectToStores`.
* Adds the necessary static methods `getStores()` and `getPropsFromStores()` to the decorated component.
*
* - Supports multiple stores.
* - Supports a simplified, string-based access to stores, with optional renaming of props.
* - Supports more flexible, redux-like access to stores using mapper functions.
*
* ### String notation
*
* @example
* @connect([{store: MyStore, props: ['myValue', 'anotherValue']}])
* export default class MyComponent extends React.Component {
* render() {
* const {myValue, anotherValue} = this.props;
* ...
* }
* }
*
* You can rename props using the ` as ` alias syntax
*
* @example
* @connect([{
* store: PeopleStore,
* props: ['items as people']
* }, {
* store: ProductStore,
* props: ['items as products']
* }])
* export default class MyComponent extends React.Component {
* render() {
* // this.props.people, this.props.products, ...
* }
* }
*
* ### Function notation
*
* Use mapper functions instead of strings in order to manually retrieve store values.
* The function receives the store state and the component props.
*
* @example
* @connect([{
* store: MyStore,
* props: (state, props) => {
* return {
* item: state.get('items').filter(item => item.get('id') === props.id)
* }
* }
* }])
* export default class MyComponent extends React.Component {
* render() {
* const item = this.props.item;
* }
* }
*
* Technically, you could also mix all access methods, but this defeats the purpose of simple access:
*
* @example
* @connect([{
* store: MyStore,
* props: ['someProp', 'anotherProp', (state, props) => {
* return {
* item: state.get('items').filter(item => item.get('id') === props.id)
* }
* }, 'some.nested.value as foo']
* }])
* export default class MyComponent extends React.Component {
* ...
* }
*
* There are however valid usecase for mixing access methods. For example, you might have keys that themselves contain dots.
* For example, that is the case when using `validate.js` with nested constraints and keeping validation results in the store.
* There might be an `errors` map in your storewith keys like `user.address.street`. In such a case you wouldn't be able to access those values because the dots do not
* represent the actual keyPath in the tree:
*
* @example
* @connect([{
* store,
* props: ['user.address.street', (state) => ({errors: state.getIn(['errors', 'user.address.street'])})]
* }])
*
* @see https://github.com/goatslacker/alt/blob/master/docs/utils/immutable.md
* @see https://github.com/goatslacker/alt/blob/master/src/utils/connectToStores.js
*
* @param {Array<{store: AltStore, props: Array<string>}>} definitions - A list of objects that each define a store connection
*/
/* eslint-enable */
function connect(definitions) {
return function (targetClass) {
targetClass.getStores = function () {
return definitions.map(function (def) {
return def.store;
});
};
targetClass.getPropsFromStores = function (componentProps) {
return definitions.reduce(function (result, def) {
if (typeof def.props === 'function') {
// the props definition is itself a function. return with its result.
return Object.assign(result, def.props(def.store.state, componentProps));
}
// the props definition is an array. evaluate and reduce each of its elements
return def.props.reduce(function (result, accessor) {
return Object.assign(result, mapProps(accessor, def.store.state, componentProps));
}, result);
}, {});
};
return (0, _connectToStores2.default)(targetClass);
};
}
function mapProps(accessor, state, props) {
switch (typeof accessor === 'undefined' ? 'undefined' : _typeof(accessor)) {
case 'function':
return mapFuncAccessor(accessor, state, props);
case 'string':
return mapStringAccessor(accessor, state);
}
}
function mapFuncAccessor(accessor, state, props) {
return accessor(state, props);
}
function mapStringAccessor(accessor, state) {
var _parseAccessor = parseAccessor(accessor),
keyPath = _parseAccessor.keyPath,
propName = _parseAccessor.propName;
return _defineProperty({}, propName, state.getIn(keyPath));
}
/**
* Takes the accessor defined by the component and retrieves `keyPath` and `propName`
* The accessor may be the name of a top-level value in the store, or a path to a nested value.
* Nested values can be accessed using a dot-separated syntax (e.g. `some.nested.value`).
*
* The name of the prop received by the component is the last part of the accessor in case of
* a nested syntax, or the accessor itself in case of a simple top-level accessor.
*
* If you need to pass the value using a different prop name, you can use the ` as ` alias syntax,
* e.g. `someProp as myProp` or `some.prop as myProp`.
*
* examples:
*
* 'someValue' // {keyPath: ['someValue'], propName: 'someValue'}
* 'someValue as foo' // {keyPath: ['someValue'], propName: 'foo'}
* 'some.nested.value' // {keyPath: ['some', 'nested', 'value'], propName: 'value'}
* 'some.nested.value as foo' // {keyPath: ['some', 'nested', 'value'], propName: 'foo'}
*
* @param {string} string - The value accessor passed by the component decorator.
* @return {object} result - A `{storeName, propName}` object
* @return {string} result.keyPath - An immutablejs keyPath array to the value in the store
* @return {string} result.propName - name for the prop as expected by the component
*/
function parseAccessor(accessor) {
var keyPath = void 0,
propName = void 0;
if (accessor.indexOf(' as ') > -1) {
// e.g. 'foo as bar' or 'some.foo as bar'
var parts = accessor.split(' as ');
keyPath = parts[0].split('.');
propName = parts[1];
} else {
// e.g. 'foo' or 'some.foo'
keyPath = accessor.split('.');
propName = keyPath[keyPath.length - 1];
}
return { keyPath: keyPath, propName: propName };
}
function connectAlternative(store, mapStateToProps, WrappedComponent) {
return function (_React$Component) {
_inherits(Connect, _React$Component);
function Connect(props, context) {
_classCallCheck(this, Connect);
var _this = _possibleConstructorReturn(this, (Connect.__proto__ || Object.getPrototypeOf(Connect)).call(this, props, context));
_this.handleStoreUpdate = function (state) {
if (_this._isMounted) {
_this.setState({ storeState: mapStateToProps(state, _this.props) });
}
};
var storeState = store.getState();
_this.state = { storeState: mapStateToProps(storeState, props) };
return _this;
}
_createClass(Connect, [{
key: 'componentDidMount',
value: function componentDidMount() {
this._isMounted = true;
this.storeSubscription = store.listen(this.handleStoreUpdate);
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this._isMounted = false;
if (this.storeSubscription) {
store.unlisten(this.storeSubscription);
}
}
// if we use props in mapStateToProps,
// we need to run it again when props have changed
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
//untested! should work though
if (mapStateToProps.length > 1) {
this.setState({ storeState: mapStateToProps(store.getState(), nextProps) });
}
}
}, {
key: 'render',
value: function render() {
var mergedProps = _extends({}, this.props, this.state.storeState);
return (0, _react.createElement)(WrappedComponent, mergedProps);
}
}]);
return Connect;
}(_react2.default.Component);
}
exports.connectAlternative = connectAlternative;