react-controllable
v0.1.1
Published
Higher order component to manage control-related events and state.
Downloads
36
Maintainers
Readme
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
: boolWhen the control currently has browser focus, is set to
true
.beacon
: boolIndicates 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 tofalse
when it loses focus or receives a click/touch.selecting
:{x, y}
orfalse
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
: boolIndicates 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'sacting
to false in the meantime.When the control is disabled or the control has no
controlPrimaryAction
method, this will always befalse
.By default, hitting the Esc key before releasing the button which caused
acting
to become true will result incontrolPrimaryAction
not being called. This can be configured using the decorator'skeyBehaviors
option (see below).hover
: boolIndicates that the user has hovered the mouse over your component's
target
(see below), and not yet hovered out.disabled
: boolIndicates 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 thecontrolState
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
refProps which aren't set on
propTypes
, except for the style-related props passed through to the shell component (style
andclassName
)The property passthrough can be configured with the
controllable
decorator'spassthrough
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
ArrayAn array of props to not pass through, even if not set on
propTypes
force
ArrayAn 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
- marks component as
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
- marks component as
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.