UNPKG

@xailabs/altx

Version:

Flux flavor based on alt.js

268 lines (236 loc) 11.5 kB
'use strict'; 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;