Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide method to debounce changes #4

Open
evanrs opened this issue Feb 22, 2016 · 2 comments
Open

Provide method to debounce changes #4

evanrs opened this issue Feb 22, 2016 · 2 comments

Comments

@evanrs
Copy link
Owner

evanrs commented Feb 22, 2016

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
Copy link
Owner Author

evanrs commented Feb 22, 2016

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
Copy link
Owner Author

evanrs commented Feb 22, 2016

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant