evanrs / redux-namespace

Namespace component state into a Redux store
MIT License
18 stars 3 forks source link

Provide method to debounce changes #4

Open evanrs opened 8 years ago

evanrs commented 8 years ago

Provide a method to debounce changes to assigned fields

this.props.debounce('fieldName', 200)

returns a referentially transparent function keyed off method name that is consistent across renders.

evanrs commented 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.

evanrs commented 8 years ago

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;
}