diff --git a/src/next/connect.js b/src/next/connect.js index d035bdf..67b3762 100644 --- a/src/next/connect.js +++ b/src/next/connect.js @@ -1,7 +1,10 @@ import { Component, PropTypes, createElement } from 'react' import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' -import result from 'lodash/result'; +import toPath from 'lodash/toPath'; +import isFunction from 'lodash/isFunction'; +import isString from 'lodash/isString'; +import memoize from 'lodash/memoize'; import { create } from './create' @@ -12,11 +15,20 @@ const storeShape = PropTypes.shape({ getState: PropTypes.func.isRequired }); -export function connect(namespace) { + +const connectNamespace = memoize(create); + + +export function connect(namespace, reducer) { + invariant(isString(namespace) || isFunction(namespace), + `Expected "namespace" to be of type string or function` + ); + return function wrapWithComponent (WrappedComponent) { class Connect extends Component { constructor(props, context) { - super(props, context) + super(...arguments); + this.store = props.store || context.store invariant(this.store, @@ -24,27 +36,44 @@ export function connect(namespace) { `props of "${this.constructor.displayName}". ` ) - this.childProps = create(namespace, this.store) + this.namespace = this.getNamespace(props, this.store) this.state = { - namespace: result(this.store.getState(), `namespace.${namespace}`, {}), - version: 0 + version: this.namespace.version() } } + getNamespace(props=this.props, store=this.store) { + return ( + connectNamespace( + [ 'namespace', + ...toPath( + isFunction(namespace) ? + namespace(state, props) : namespace) ], + store + ) + ) + } + componentDidMount() { - this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) + if (! this.unsubscribe) { + this.unsubscribe = + this.store.subscribe(this.handleChange.bind(this)); + this.handleChange(); + } } componentWillUnmount() { - this.unsubscribe() - this.unsubscribe = null + if (this.unsubscribe) { + // comma operator, because why not? + this.unsubscribe = this.unsubscribe(), null; + } } componentWillUpdate(nextProps, nextState) { if (nextState.version !== this.state.version) { - this.childProps = { - ...this.childProps, - version: nextState.version + this.namespace = { + ...this.getNamespace(nextProps), + _version: nextState.version } } } @@ -54,19 +83,17 @@ export function connect(namespace) { return } - const prev = this.state.namespace - const next = result(this.store.getState(), `namespace.${namespace}`, prev) + const prev = this.state.version + const next = this.namespace.version(); + if (prev !== next) { - this.setState({ - namespace: next, - version: this.state.version + 1 - }) + this.setState({ version: next }) } } render() { - return createElement(WrappedComponent, { ...this.props, [namespace]: this.childProps }) + return createElement(WrappedComponent, { ...this.props, [namespace]: this.namespace }) } } diff --git a/src/next/create.js b/src/next/create.js index 6604dec..27e4f7e 100644 --- a/src/next/create.js +++ b/src/next/create.js @@ -1,9 +1,15 @@ +import flow from 'lodash/flow'; +import get from 'lodash/get'; import isFunction from 'lodash/isFunction'; +import isNil from 'lodash/isNil'; +import isObject from 'lodash/isObject'; import isString from 'lodash/isString'; -import result from 'lodash/result'; -import flow from 'lodash/flow'; -import property from 'lodash/property'; import mapValues from 'lodash/mapValues'; +import property from 'lodash/property'; +import result from 'lodash/result'; +import toPath from 'lodash/toPath'; + +import invariant from 'invariant'; import { BIND } from './reducer'; @@ -23,8 +29,12 @@ export function assign(namespace, key, value) { export function create(namespace, store) { const { dispatch, getState } = store; + + const selectNamespace = + property(['namespace', ...toPath(namespace)]) + const getNamespace = - flow(getState, property(['namespace', namespace])); + flow(getState, selectNamespace); function selector(key, __) { return arguments.length > 0 ? @@ -38,6 +48,7 @@ export function create(namespace, store) { // curry assign with target isString(target) ? dispatcher.bind(this, target) + // TODO interpret array as property.path // map target ({key: value}) => assign : mapValues(target, (value, key) => dispatcher(key, value)) // deferred selector @@ -51,7 +62,7 @@ export function create(namespace, store) { ) } - return { + const ns = { assign: dispatcher, assigns(key, selector) { return dispatcher(key, (value, ...args) => @@ -60,13 +71,31 @@ export function create(namespace, store) { : value ) }, + cursor(path, defaultValue={}) { + let nspath = toPath([toPath(namespace), toPath(path)]) + let cursor = create(nspath, store); + + return cursor; + }, dispatch, select: selector, selects() { return selector.bind(null, ...arguments); }, touched(key) { - return selector(['@@touched'].concat(key), false); + return selector(['@@touched', ...toPath(key)], false); + }, + reset(key) { + dispatcher(key, null); + dispatcher(['@@touched', ...toPath(key)], null); + }, + resets(key) { + return ns.reset.bind(ns, key); + }, + version() { + return selector('@@version', 0) } } + + return ns; } diff --git a/src/next/reducer.js b/src/next/reducer.js index e466ac9..fe6cc31 100644 --- a/src/next/reducer.js +++ b/src/next/reducer.js @@ -1,19 +1,51 @@ -import result from 'lodash/result'; +import concat from 'lodash/concat'; +import get from 'lodash/get'; +import merge from 'lodash/merge'; +import set from 'lodash/set'; +import toPath from 'lodash/toPath'; +import clone from 'lodash/clone'; export const BIND = 'BIND_NAMESPACE_NEXT'; + export function namespaceReducer (state={}, action={}) { + if (action.type === BIND) { let { payload: { namespace, key, value } } = action - let prev = result(state, namespace, {}); - let touched = result(prev, '@@touched', {}); - let next = { - ...prev, [key]: value, ['@@touched']: { ...touched, [key]: true } }; + namespace = toPath(namespace); + key = toPath(key); + + let changedPath = concat(namespace, key); + let touchedPath = concat(namespace, '@@touched', key); + let versionPath = concat(namespace, '@@version'); + + if (value !== get(state, changedPath)) { + let version = get(state, versionPath, 0) + 1; + let fragment = set({}, namespace, get(state, namespace)); + + clonePath(fragment, changedPath); + clonePath(fragment, touchedPath); + + set(fragment, versionPath, version); + set(fragment, changedPath, value); + set(fragment, touchedPath, true); - state = { ...state, [namespace]: next }; + state = merge(clone(state), fragment) + } } + return state; } + + +function clonePath (target, path) { + path.forEach((key, idx, col) => { + key = col.slice(0, idx); + set(target, key, clone(get(target, key))) + }) + + return target; +}