react-controllable

0.1.1 • Public • Published

react-controllable

react-controllable is a foundation for React components which accept user input.

Reasons to use react-controllable:

  • Don't manually manage input-related state ever again
  • Access all the input-related state you'll ever need in a tidy controlState object
  • Great test coverage
  • Create controls faster than ever before!
  • Easily create themeable controls by combining with react-themesmith
  • The standard controlState format lets you write components which work across controls

Reasons not to use react-controllable:

  • You need your control to be really, really performant
  • You need to add your own event handlers and can't use ES7 decorators

*Using Babel? To unlock ES7 decorators, you'll need to enable stage 0 features. If you're using Babel with webpack (and you probably should be), see this article for instructions on how to do this.

Getting started

Install with:

npm install react-controllable --save

And import or require:

// ES6
import controllable from 'react-controllable'
 
// ES5
var controllable = require('controllable')

Apply controllable to your component class

controllable is the Higher-Order Component which makes the magic happen. You can apply it as an ES7 decorator:

@controllable()
class MyComponent extends React.Component {
  ...
}

Or if you're stuck with ES5/6, just wrap your component in it:

// With ES6 classes
const MyComponent = controllable()(class MyComponent extends React.Component {
  ...
})
 
// With `React.createClass`
const MyComponent = controllable()(React.createClass({
  ...
}))

Access current state on this.controlState

Each controllable component has a read-only this.controlState object. This object contains the latest control-related state, automatically managed for your convenience. Properties available:

  • active: bool

    When the control currently has browser focus, is set to true.

  • beacon: bool

    Indicates that we should make the control stand out to the user.

    Is set to true when the control receives focus due to keyboard input, and set to false when it loses focus or receives a click/touch.

  • selecting: {x, y} or false

    Indicates that the user is currently pressing your component's target (see below), using a pointing device like a mouse or finger.

    If the user is pressing, the {x, y} object will reflect the position of the click/touch attached to the associated event object.

    When the control is disabled, this will always be false.

  • acting: bool

    Indicates that the user has pressed a button associated with the component's primary action. When this button is released, your component's controlPrimaryAction method will be called, unless something else set's acting to false in the meantime.

    When the control is disabled or the control has no controlPrimaryAction method, this will always be false.

    By default, hitting the Esc key before releasing the button which caused acting to become true will result in controlPrimaryAction not being called. This can be configured using the decorator's keyBehaviors option (see below).

  • hover: bool

    Indicates that the user has hovered the mouse over your component's target (see below), and not yet hovered out.

  • disabled: bool

    Indicates that this control cannot currently be selected or acted upon.

    This property mirrors your prop's this.props.disabled - it is added for convenience, so you can pass the controlState object as-is for a complete picture of your controllable component's state.

Add lifecycle methods if required

  • controlPrimaryAction(e)

Some controls have a fairly obvious primary action - for example toggling a checkbox or pressing a button.

In the case your control has one of these actions, when react-controllable wants to call it, it'll call your component's controlPrimaryAction method (if it exists), passing the event object from the click/keypress which caused it.

  • controlWillUpdate(nextControlState)

This method will be called before this.controlState is updated, passing the next version as the argument.

Set up your render method

To get this automagic controlState object, you'll need to add some props to the components in your render function. In particular, you'll want to add the results of these three methods: (see below for an example)

this.shell()

This method passes through a shell ref, and style-related props like className and style.

Add it to the outermost element you render, for example:

render() {
  return <div {...this.shell()}>...</div>
}

this.target()

This method passes through a target ref, as well as callbacks which need to be active on the control's click/touch target (for example callbacks for mouse and touch events).

If your focusable element accepts children (e.g. a, button, etc.), the convention is to place this directly inside it, and style it to fit snugly inside - this way any clicks on the target will always cause an event to hit the main DOM element.

In the case you can't add it as a child (e.g. for input), make sure you take into account that clicks on the target may not automatically bring focus to your main DOM Element.

See below for usage exmples.

this.focusable()

This method passes through three things:

  • Callbacks for your main DOM element (e.g. for keyboard events).

  • A focusable ref

  • Props which aren't set on propTypes, except for the style-related props passed through to the shell component (style and className)

    The property passthrough can be configured with the controllable decorator's passthrough option - see below for details.

Usage Examples

If your focusable element can have children, the target should be direectly inside it. Otherwise it should be direcetly outside it.

Given this rule and knowing what focusable element you'll use, you should generally be able to set up your render by copying the structure of one of these examples:

Button (shell > focusable > target)

Based on sui-button

import React from "react"
import ReactDOM from "react-dom"
import controllable from "react-controllable"
 
@controllable({
  keyBehaviors: {
    'SPACE': 'act',
    'ENTER': 'act',
  },
  passthrough: {
    force: ['type']
  },
})
export default class Button extends React.Component {
  static propTypes = {
    onPress: React.PropTypes.func,
    type: React.PropTypes.string,
    theme: React.PropTypes.object.isRequired,
    children: React.PropTypes.node.isRequired,
  }
 
  static defaultProps = {
    type: 'button',
  }
 
  controlPrimaryAction() {
    const focusable = ReactDOM.findDOMNode(this.refs.focusable)
    if (this.props.type == 'submit' && focusable.form) {
      focusable.form.dispatchEvent(new Event('submit'))
    }
    if (this.props.onPress) {
      this.props.onPress()
    }
  }
 
  render() {
    const {Shell, Target, Content} = this.props.theme
    return (
      <Shell {...this.props.shell()}>
        <button {...this.props.focusable()}>
          <Target {...this.props.target()}>
            <Content controlState={this.controlState}>
              {this.props.children}
            </Content>
          </Target>
        </button>
      </Shell>
    )
  }
}

Text field (shell > target > focusable)

Based on sui-input

TODO

Other configuration

The controllable function accepts an options object, with the following options:

passthrough

Allows you to configure the props which are passed through to your focusable element with this.focusable(), by accepting an object with the following options:

  • omit Array

    An array of props to not pass through, even if not set on propTypes

  • force Array

    An array of props to pass through, even if set on propTypes

This uses the react-passthrough higher-order component under the hood.

keyBehaviors

Behaviors can be bound to keys using the keyBehaviors option:

  • cancel (bound to ESC by default):

    • removes beacon (but does not deactivate)
    • removes any pointer selection info
    • cancels action if we're currently "acting"
  • act (generally used with SPACE, but not bound by default):

    • marks component as acting on press
    • does not update select
    • if component has beacon, hides it only while pressing
    • runs primary action on release
  • selectAndAct (bound to pointing devices by default):

    • marks component as acting on press
    • while pressed, select contains the {x, y} measured relative to the viewport
    • press removes beacon
    • runs primary action on release
  • submitOrAct (generally used with ENTER, but not bound by default):

    • if the component is part of a form, press submits form and removes beacon
    • if the component is not part of a form, delegates to Act

The acceptable key names are: ENTER, SPACE, ESC.

Example:

@controllable({keyBehaviors: {ENTER: 'submitOrAct', SPACE: 'act'}})
class CheckBoxComponent extends Component {
  ...
}

Handling events manually

TODO: (react-controllable handles a lot of callbacks, so you don't want to add these the normal way. Instead use the handy dandy @controllable.on decorator...)

This uses the react-callback-register higher-order component under the hood.

Contributing

TODO: clone, npm install, npm test or karma start. Let me know before you work on any new features, I don't want to make the project much bigger than it already is.

Package Sidebar

Install

npm i react-controllable

Weekly Downloads

32

Version

0.1.1

License

MIT

Last publish

Collaborators

  • jamesknelson