Skip to content

Commit

Permalink
Changes scroll strategy to use onFocus and onBlur, since keyboard eve…
Browse files Browse the repository at this point in the history
…nts does not work well on Android with android:windowSoftInputMode="adjustNothing"
  • Loading branch information
danielweinmann committed Feb 24, 2016
1 parent 7182a02 commit 1d9680d
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 46 deletions.
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ It implements the most common pattern of mobile form user interaction by convens

- React Native 0.20+
- iOS
- Android
- Android (see installation below)

## Inspiration

Expand All @@ -39,6 +39,10 @@ The reason for creating a new package is that I want the form components to be p

```npm install react-native-stateless-form --save```

#### Android

You should add `android:windowSoftInputMode="adjustNothing"` attribute to the `<activity>` tag with `android:name=".MainActivity"` in your `AndroidManifest.xml`. Otherwise, it will have duplicate scroll behaviour.

## Examples

#### The dirtiest example using React state
Expand Down Expand Up @@ -137,11 +141,15 @@ import Icon from 'react-native-vector-icons/MaterialIcons'
import { StatelessForm, InlineTextInput } from 'react-native-stateless-form'

class FormInput extends Component {
// You MUST implement focus method for your widget to work
// You MUST implement focus and blur methods for your widget to work
focus() {
this.refs.input.focus()
}

blur() {
this.refs.input.blur()
}

render() {
const { iconName } = this.props
return (
Expand Down Expand Up @@ -265,6 +273,10 @@ class FormInput extends Component {
this.refs.input.focus()
}

blur() {
this.refs.input.blur()
}

render() {
const { iconName, name, value } = this.props
const { valid, messages } = validate(UserValidators[name], value)
Expand Down Expand Up @@ -394,6 +406,10 @@ class FormInput extends Component {
this.refs.input.focus()
}

blur() {
this.refs.input.blur()
}

render() {
const { iconName, name, value, error } = this.props
const message = ( error && error.length > 0 ? error[0] : null)
Expand Down Expand Up @@ -532,15 +548,16 @@ If you want your widget to receive focus when previous widget finished editing,

- Your widget must have a component created with ES6 `class` statement.
- Your widget should implement the `focus()` method.
- Your widget should implement the `blur()` method.
- Your widget should implement `onSubmitEditing` or equivalent and call `this.props.onNextInputFocus(this.props.nextInput, this)` so StatelessForm can focus the next input or blur the current input.
- You must pass the component's class name to your form's `focusableTypes` prop.

#### Scrollable input widgets

If you want your widget to receive scroll when showing keyboard, you must implement the following pattern:

- Your widget should implement keyboard show management and call `this.props.onKeyboardShow({ height, y, keyboardHeight })` on keyboard show. `height` must be your widget's height, `y` must be your widget y position and `keyboardHeight` must be the currently open keyboard height.
- Your widget should implement keyboard hide management and call `this.props.onKeyboardHide()` on keyboard hide.
- Check [InlineTextInput](https://github.com/danielweinmann/react-native-stateless-form/blob/master/widgets/InlineTextInput.js) for references on how to implement it.
- Your widget should implement `onFocus` and call `this.props.onFocus(scrollTo)` on focus. `scrollTo` must be your widget's `y` position.
- You can get your `y` position using `onLayout` prop. Check [InlineTextInput](https://github.com/danielweinmann/react-native-stateless-form/blob/master/widgets/InlineTextInput.js) for references on how to implement it.

## Contributing

Expand Down
28 changes: 19 additions & 9 deletions StatelessForm.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { Component, PropTypes, ScrollView } from 'react-native'
import React, { Platform, Component, PropTypes, ScrollView, View } from 'react-native'

export default class StatelessForm extends Component {
componentDidMount() {
this.willFocusInput = false
}

childrenWithProps() {
const { focusableTypes } = this.props
let nextInput = null
Expand All @@ -12,8 +16,8 @@ export default class StatelessForm extends Component {
ref: `input${inputCount}`,
nextInput: nextInput,
onNextInputFocus: this.handleNextInputFocus.bind(this),
onKeyboardShow: this.handleKeyboardShow.bind(this),
onKeyboardHide: this.handleKeyboardHide.bind(this),
onFocus: this.handleFocus.bind(this),
onBlur: this.handleBlur.bind(this),
})
nextInput = input
return input
Expand All @@ -23,21 +27,26 @@ export default class StatelessForm extends Component {
}).reverse()
}

handleNextInputFocus(nextInput) {
handleNextInputFocus(nextInput, currentInput) {
if (nextInput) {
const input = this.refs[nextInput.ref]
this.willFocusInput = true
input.focus()
} else {
this.handleKeyboardHide()
currentInput.blur()
}
}

handleKeyboardHide() {
this.refs.scrollView.scrollTo({y: 0})
handleBlur() {
if (!this.willFocusInput) {
this.refs.scrollView.scrollTo({y: 0})
}
this.willFocusInput = false
}

handleKeyboardShow({ height, y, keyboardHeight }) {
this.refs.scrollView.scrollTo({y: y})
handleFocus(scrollTo) {
this.willFocusInput = false
this.refs.scrollView.scrollTo({y: scrollTo})
}

render() {
Expand All @@ -52,6 +61,7 @@ export default class StatelessForm extends Component {
}, this.props.style]}
>
{this.childrenWithProps()}
{ Platform.OS == 'android' && <View style={{ height: 500 }}/> }
</ScrollView>
)
}
Expand Down
45 changes: 13 additions & 32 deletions widgets/InlineTextInput.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,35 @@
import React, { Component, PropTypes, DeviceEventEmitter, View, Text, TextInput } from 'react-native'
import React, { Component, PropTypes, View, Text, TextInput } from 'react-native'

export default class InlineTextInput extends Component {
componentDidMount() {
this.layout = { x: 0, y: 0, width: 0, height: 0 }
this.willShowKeyboard = false
this.isShowingKeyboard = false
DeviceEventEmitter.addListener('keyboardDidShow', this.handleKeyboardShow.bind(this))
DeviceEventEmitter.addListener('keyboardDidHide', this.handleKeyboardHide.bind(this))
this.scrollTo = 0
}

handleLayout(event) {
this.layout = event.nativeEvent.layout
this.scrollTo = event.nativeEvent.layout.y
}

handleFocus(event) {
handleFocus() {
const { onFocus } = this.props
this.willShowKeyboard = true
onFocus && onFocus()
}

handleKeyboardShow(frames) {
const keyboardHeight = frames.endCoordinates.height
const { height, y } = this.layout
if (this.willShowKeyboard) {
const { onKeyboardShow } = this.props
onKeyboardShow && onKeyboardShow({ height, y, keyboardHeight })
this.isShowingKeyboard = true
this.willShowKeyboard = false
}
}

handleKeyboardHide() {
if (this.isShowingKeyboard) {
const { onKeyboardHide } = this.props
onKeyboardHide && onKeyboardHide()
this.isShowingKeyboard = false
}
onFocus && onFocus(this.scrollTo)
}

focus() {
this.refs.input.focus()
}

blur() {
this.refs.input.blur()
}

shouldDisplayMessage() {
const { value, valid, message } = this.props
return (value && value.length > 0 && !valid && message)
}

handleSubmitEditing() {
const { nextInput, onNextInputFocus } = this.props
onNextInputFocus && onNextInputFocus(nextInput)
onNextInputFocus && onNextInputFocus(nextInput, this)
}

renderIcon() {
Expand All @@ -74,7 +54,7 @@ export default class InlineTextInput extends Component {
}

renderMessage() {
const { value, valid, message, messageStyle } = this.props
const { message, messageStyle } = this.props
if (this.shouldDisplayMessage()) {
return(
<Text style={[{
Expand All @@ -90,7 +70,7 @@ export default class InlineTextInput extends Component {
}

render() {
const { title, value, valid, message, style, titleStyle, inputStyle, messageStyle, nextInput } = this.props
const { title, value, style, titleStyle, inputStyle, nextInput, onBlur } = this.props
return (
<View
onLayout={this.handleLayout.bind(this)}
Expand Down Expand Up @@ -124,6 +104,7 @@ export default class InlineTextInput extends Component {
onSubmitEditing={this.handleSubmitEditing.bind(this)}
{ ...this.props }
onFocus={this.handleFocus.bind(this)}
onBlur={onBlur}
ref='input'
value={value}
style={[{
Expand Down

0 comments on commit 1d9680d

Please sign in to comment.