Skip to content

Commit 93b8cc7

Browse files
author
Evan Schneider
committed
Merge pull request #10 from evanrs/next
Add cursors, coerce namespace and keys to property paths
2 parents 9ef805a + 639c076 commit 93b8cc7

File tree

3 files changed

+119
-31
lines changed

3 files changed

+119
-31
lines changed

src/next/connect.js

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Component, PropTypes, createElement } from 'react'
22
import hoistStatics from 'hoist-non-react-statics'
33
import invariant from 'invariant'
4-
import result from 'lodash/result';
4+
import toPath from 'lodash/toPath';
5+
import isFunction from 'lodash/isFunction';
6+
import isString from 'lodash/isString';
7+
import memoize from 'lodash/memoize';
58

69
import { create } from './create'
710

@@ -12,39 +15,65 @@ const storeShape = PropTypes.shape({
1215
getState: PropTypes.func.isRequired
1316
});
1417

15-
export function connect(namespace) {
18+
19+
const connectNamespace = memoize(create);
20+
21+
22+
export function connect(namespace, reducer) {
23+
invariant(isString(namespace) || isFunction(namespace),
24+
`Expected "namespace" to be of type string or function`
25+
);
26+
1627
return function wrapWithComponent (WrappedComponent) {
1728
class Connect extends Component {
1829
constructor(props, context) {
19-
super(props, context)
30+
super(...arguments);
31+
2032
this.store = props.store || context.store
2133

2234
invariant(this.store,
2335
`Could not find "store" in either the context or ` +
2436
`props of "${this.constructor.displayName}". `
2537
)
2638

27-
this.childProps = create(namespace, this.store)
39+
this.namespace = this.getNamespace(props, this.store)
2840
this.state = {
29-
namespace: result(this.store.getState(), `namespace.${namespace}`, {}),
30-
version: 0
41+
version: this.namespace.version()
3142
}
3243
}
3344

45+
getNamespace(props=this.props, store=this.store) {
46+
return (
47+
connectNamespace(
48+
[ 'namespace',
49+
...toPath(
50+
isFunction(namespace) ?
51+
namespace(state, props) : namespace) ],
52+
store
53+
)
54+
)
55+
}
56+
3457
componentDidMount() {
35-
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
58+
if (! this.unsubscribe) {
59+
this.unsubscribe =
60+
this.store.subscribe(this.handleChange.bind(this));
61+
this.handleChange();
62+
}
3663
}
3764

3865
componentWillUnmount() {
39-
this.unsubscribe()
40-
this.unsubscribe = null
66+
if (this.unsubscribe) {
67+
// comma operator, because why not?
68+
this.unsubscribe = this.unsubscribe(), null;
69+
}
4170
}
4271

4372
componentWillUpdate(nextProps, nextState) {
4473
if (nextState.version !== this.state.version) {
45-
this.childProps = {
46-
...this.childProps,
47-
version: nextState.version
74+
this.namespace = {
75+
...this.getNamespace(nextProps),
76+
_version: nextState.version
4877
}
4978
}
5079
}
@@ -54,19 +83,17 @@ export function connect(namespace) {
5483
return
5584
}
5685

57-
const prev = this.state.namespace
58-
const next = result(this.store.getState(), `namespace.${namespace}`, prev)
86+
const prev = this.state.version
87+
const next = this.namespace.version();
88+
5989

6090
if (prev !== next) {
61-
this.setState({
62-
namespace: next,
63-
version: this.state.version + 1
64-
})
91+
this.setState({ version: next })
6592
}
6693
}
6794

6895
render() {
69-
return createElement(WrappedComponent, { ...this.props, [namespace]: this.childProps })
96+
return createElement(WrappedComponent, { ...this.props, [namespace]: this.namespace })
7097
}
7198
}
7299

src/next/create.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import flow from 'lodash/flow';
2+
import get from 'lodash/get';
13
import isFunction from 'lodash/isFunction';
4+
import isNil from 'lodash/isNil';
5+
import isObject from 'lodash/isObject';
26
import isString from 'lodash/isString';
3-
import result from 'lodash/result';
4-
import flow from 'lodash/flow';
5-
import property from 'lodash/property';
67
import mapValues from 'lodash/mapValues';
8+
import property from 'lodash/property';
9+
import result from 'lodash/result';
10+
import toPath from 'lodash/toPath';
11+
12+
import invariant from 'invariant';
713

814
import { BIND } from './reducer';
915

@@ -23,8 +29,12 @@ export function assign(namespace, key, value) {
2329

2430
export function create(namespace, store) {
2531
const { dispatch, getState } = store;
32+
33+
const selectNamespace =
34+
property(['namespace', ...toPath(namespace)])
35+
2636
const getNamespace =
27-
flow(getState, property(['namespace', namespace]));
37+
flow(getState, selectNamespace);
2838

2939
function selector(key, __) {
3040
return arguments.length > 0 ?
@@ -38,6 +48,7 @@ export function create(namespace, store) {
3848
// curry assign with target
3949
isString(target) ?
4050
dispatcher.bind(this, target)
51+
// TODO interpret array as property.path
4152
// map target ({key: value}) => assign
4253
: mapValues(target, (value, key) => dispatcher(key, value))
4354
// deferred selector
@@ -51,7 +62,7 @@ export function create(namespace, store) {
5162
)
5263
}
5364

54-
return {
65+
const ns = {
5566
assign: dispatcher,
5667
assigns(key, selector) {
5768
return dispatcher(key, (value, ...args) =>
@@ -60,13 +71,31 @@ export function create(namespace, store) {
6071
: value
6172
)
6273
},
74+
cursor(path, defaultValue={}) {
75+
let nspath = toPath([toPath(namespace), toPath(path)])
76+
let cursor = create(nspath, store);
77+
78+
return cursor;
79+
},
6380
dispatch,
6481
select: selector,
6582
selects() {
6683
return selector.bind(null, ...arguments);
6784
},
6885
touched(key) {
69-
return selector(['@@touched'].concat(key), false);
86+
return selector(['@@touched', ...toPath(key)], false);
87+
},
88+
reset(key) {
89+
dispatcher(key, null);
90+
dispatcher(['@@touched', ...toPath(key)], null);
91+
},
92+
resets(key) {
93+
return ns.reset.bind(ns, key);
94+
},
95+
version() {
96+
return selector('@@version', 0)
7097
}
7198
}
99+
100+
return ns;
72101
}

src/next/reducer.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
1-
import result from 'lodash/result';
1+
import concat from 'lodash/concat';
2+
import get from 'lodash/get';
3+
import merge from 'lodash/merge';
4+
import set from 'lodash/set';
5+
import toPath from 'lodash/toPath';
6+
import clone from 'lodash/clone';
27

38

49
export const BIND = 'BIND_NAMESPACE_NEXT';
510

11+
612
export function namespaceReducer (state={}, action={}) {
13+
714
if (action.type === BIND) {
815
let { payload: { namespace, key, value } } = action
916

10-
let prev = result(state, namespace, {});
11-
let touched = result(prev, '@@touched', {});
12-
let next = {
13-
...prev, [key]: value, ['@@touched']: { ...touched, [key]: true } };
17+
namespace = toPath(namespace);
18+
key = toPath(key);
19+
20+
let changedPath = concat(namespace, key);
21+
let touchedPath = concat(namespace, '@@touched', key);
22+
let versionPath = concat(namespace, '@@version');
23+
24+
if (value !== get(state, changedPath)) {
25+
let version = get(state, versionPath, 0) + 1;
26+
let fragment = set({}, namespace, get(state, namespace));
27+
28+
clonePath(fragment, changedPath);
29+
clonePath(fragment, touchedPath);
30+
31+
set(fragment, versionPath, version);
32+
set(fragment, changedPath, value);
33+
set(fragment, touchedPath, true);
1434

15-
state = { ...state, [namespace]: next };
35+
state = merge(clone(state), fragment)
36+
}
1637
}
1738

39+
1840
return state;
1941
}
42+
43+
44+
function clonePath (target, path) {
45+
path.forEach((key, idx, col) => {
46+
key = col.slice(0, idx);
47+
set(target, key, clone(get(target, key)))
48+
})
49+
50+
return target;
51+
}

0 commit comments

Comments
 (0)