Skip to content

Instantly share code, notes, and snippets.

@loopmode
Last active May 22, 2017 07:35
Show Gist options
  • Save loopmode/4babe4cc99ff4bdb7d939f14f3f1d56d to your computer and use it in GitHub Desktop.
Save loopmode/4babe4cc99ff4bdb7d939f14f3f1d56d to your computer and use it in GitHub Desktop.
A HOC decorator for binding react components to alt.js immutable stores
import connectToStores from 'alt-utils/lib/connectToStores';
/* 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;
* ...
* }
* }
*
* ### Aliases via ` as ` syntax
*
* 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, ...
* }
* }
*
* ### Accessing deeply nested properties
*
* You can access nested properties using dot-syntax. The string is then split by dots
* and the resulting keyPath array is used with immutable's `getIn()` method.
* If you do not specify an alias via ` as ` syntax, your component receives the prop by the last segment of the keyPath.
* So, if you bind to `currentUser.address.street`, your component receives a prop `street`.
* However, you can also bind to `currentUser.address.street as foo` and receive the value as prop `foo`.
*
* @example
* @connect([{
* store: ApplicationStore,
* props: [
* 'calculator.options',
* 'calculator.selectedIndex',
* 'calculator.product.interestRate',
* 'product.isCarLoan as showBanner'
* ]
* }])
* export default class MyComponent extends React.Component {
* ...
* }
*
* ## 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
*
* # Usage without decorator syntax
*
* @example
* const connectStores = connect([[{
* store: MyStore,
* props: ['a', b', 'foo.bar']
* }]]);
* class MyComponent extends React.Component {...}
* export default connectStores(MyComponent);
*/
/* eslint-enable */
function connect(definitions) {
return function(targetClass) {
targetClass.getStores = function() {
return definitions.map((def) => def.store);
};
targetClass.getPropsFromStores = function(componentProps) {
return definitions.reduce((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((result, accessor) => {
return Object.assign(result, mapProps(accessor, def.store.state, componentProps));
}, result);
}, {});
};
return connectToStores(targetClass);
};
}
function mapProps(accessor, state, props) {
switch (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) {
const {keyPath, propName} = parseAccessor(accessor);
return {
[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) {
let keyPath, propName;
if (accessor.indexOf(' as ') > -1) {
// e.g. 'foo as bar' or 'some.foo as bar'
const 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, propName};
}
export default connect;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment