Open evanrs opened 8 years ago
The debounce property should live on the NamespaceComponent instance rather than as a per field specification. For completeness sake a method to bypass the debounce and commit the changes should be provided.
A first pass implementation required field locks to mediate events when multiple fields were edited within the delta.
import React, { Component, PropTypes } from 'react';
import * as ReactRedux from 'react-redux';
import find from 'lodash.find';
import isFunction from 'lodash.isFunction';
import result from 'lodash.result';
import isString from 'lodash/isString';
import flow from 'lodash/flow';
import debounce from 'lodash/debounce';
import identity from 'lodash/identity';
export const BIND = 'BIND_NAMESPACE';
export const shape = {
assign: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
select: PropTypes.func.isRequired
}
export function assign(namespace, key, value) {
if (! key)
return (key, value) =>
assign(namespace, key, value);
let action = (value) => ({
type: BIND, payload: { namespace, key, value } })
if ([...arguments].length < assign.length)
return action;
return action(value);
}
function selectWith (selector, value) {
if (arguments.length === 1) {
return selectWith.bind(null, selector);
}
return (
isString(selector) ? result(value, selector)
: isFunction(selector) ? selector(value)
: value
)
}
export function connect(namespace, initial={}) {
return (WrappedComponent) =>
ReactRedux.connect(
({ namespace: { [namespace]: state } }) => ({
assign: assign(namespace),
select(key, __) {
return arguments.length > 0 ? result(state, key, __) : state || {}
}
})
)(class NamespaceBridge extends Component {
constructor() {
super(...arguments);
// retains debounced values
this.state = {};
this.debounced = {};
}
componentWillReceiveProps(props) {
let newState = {};
Object.keys(this.state).map((key) =>
newState[key] = props[key])
this.setState(newState);
}
render () {
let {assign, dispatch, select, ...props} = this.props;
function dispatcher(target, value) {
return (
// curry or assign many
arguments.length === 1 ?
// curry assign with target
isString(target) ?
dispatcher.bind(this, target)
// map target ({key: value}) => assign
: ( Object.keys(target).map((key) =>
dispatcher(key, target[key]))
, target )
// deferred selector
: isFunction(value) ?
(...args) => dispatcher(target, value(...args))
// memoize
: select(target) !== value ?
( dispatch(assign(target, value))
, value )
: value
)
}
props = {
// namespace defers to props
...select(),
...props,
assign: dispatcher,
assigns(key, selector) {
return dispatcher(key, selectWith(selector))
},
debounces: (key, selector, timeout) => {
if (key && ! selector) {
return result(this.state, key, props.select(key));
}
this.debounced[key] = this.debounced[key] || {
timeout,
bouncer: debounce(dispatcher(key), timeout)
}
return (value) => {
value = selectWith(selector, value);
this.debounced[key].bouncer(value);
this.setState({ [key]: value });
}
},
dispatch,
select,
selects() {
return select.bind(null, ...arguments);
},
touched(key) {
return select(['@@touched'].concat(key), false);
}
}
return React.isValidElement(WrappedComponent) ?
React.cloneElement(WrappedComponent, props) : <WrappedComponent {...props}/>
}
})
}
export function reducer (state={}, action={}) {
if (action.type === BIND) {
let { payload: { namespace, key, value } } = action
let ns = result(state, namespace, {});
let touched = result(ns, '@@touched', {});
ns = {
...ns, [key]: value, ['@@touched']: { ...touched, [key]: true } };
state = { ...state, [namespace]: ns };
}
return state;
}
Provide a method to debounce changes to assigned fields
returns a referentially transparent function keyed off method name that is consistent across renders.