diff --git a/README.md b/README.md index cc051c3..1d768af 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,17 @@ It implements the most common pattern of mobile form user interaction by convension over configuration. You'll never have to worry again about scrolling and focusing form fields. -- It uses inline form fields with icons and titles +- It uses inline form fields with icons and labels - It displays different icons for valid and invalid field values - It displays validation message inside the field - When a field receives focus, it displays a keyboard (\*) -- If it is not the last field in the form, the keyboard return key is set to `Next` (\*\*) -- If it is the last field in the form, the keyboard return key is set to `Done` and hides keaboard on return (\*\*) +- If it is not the last field in the form, the keyboard return key is set to `Next` +- If it is the last field in the form, the keyboard return key is set to `Done` and hides keaboard on return - When a field receives focus, the form scrolls to the top of the field to avoid it being hidden behind the keyboard - When all fields lose focus, the form scrolls back to the top of the form (\*) Unless an external keyboard is connected to the device -(\*\*) On Android the return key button is always displayed as `Done` for now, since React Native does not support changing it yet. But the behaviour works correctly ;) - ## What it does NOT do - It does not implement form validation. We recommend using [validate-model](https://github.com/danielweinmann/validate-model) for that. But you can use anything you want. @@ -33,7 +31,7 @@ It implements the most common pattern of mobile form user interaction by convens ## Support -- React Native 0.20+ +- React Native 0.25+ - iOS - Android (see installation below) @@ -82,10 +80,10 @@ class Form extends Component { backgroundColor: 'lightgray', }}> } @@ -97,13 +95,13 @@ class Form extends Component { onChangeText={(text) => { this.setState({name: text}) }} /> } @@ -115,13 +113,13 @@ class Form extends Component { onChangeText={(text) => { this.setState({email: text}) }} /> } @@ -141,15 +139,16 @@ import { AppRegistry } from 'react-native' AppRegistry.registerComponent('Form', () => Form) ``` -#### Create your own widget to keep it DRY +#### Create your own component to keep it DRY ```js -import React, { Component, PropTypes } from 'react-native' +import React, { Component } from 'react-native' +import PropTypes from 'prop-types' import Icon from 'react-native-vector-icons/MaterialIcons' import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' class FormInput extends Component { - // You MUST implement focus and blur methods for your widget to work + // You MUST implement focus and blur methods for your component to work focus() { this.refs.input.focus() } @@ -164,7 +163,7 @@ class FormInput extends Component { } @@ -200,7 +199,7 @@ class Form extends Component { return ( { this.setState({name: text}) }} /> { this.setState({email: text}) }} /> Form) #### Usage with validate-model ```js -import React, { Component, PropTypes } from 'react-native' +import React, { Component } from 'react-native' +import PropTypes from 'prop-types' import Icon from 'react-native-vector-icons/MaterialIcons' import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' import { validate } from 'validate-model' @@ -295,7 +295,7 @@ class FormInput extends Component { } @@ -330,7 +330,7 @@ class Form extends Component { Form) #### Usage with Redux Form ```js -import React, { Component, PropTypes } from 'react-native' +import React, { Component } from 'react-native' +import PropTypes from 'prop-types' import Icon from 'react-native-vector-icons/MaterialIcons' import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' import { validateAll } from 'validate-model' @@ -430,7 +431,7 @@ class FormInput extends Component { } @@ -455,14 +456,14 @@ class Form extends Component { Root) ## StatelessForm -A wrapper that will manage auto-focusing and auto-scrolling for its children widgets +A wrapper that will manage auto-focusing and auto-scrolling for its children components | Property | Type | Default | Description | |---------------|----------|--------------|----------------------------------------------------------------| @@ -521,19 +522,19 @@ A wrapper that will manage auto-focusing and auto-scrolling for its children wid \+ Any other [ScrollView](https://facebook.github.io/react-native/docs/scrollview.html#content) prop you wish to pass. -## Widgets +## Components #### InlineTextInput | Property | Type | Default | Description | |---------------|----------|--------------|----------------------------------------------------------------| -| title | string | 'Use title prop' | Title for the text input | +| label | string | 'Use label prop' | Label for the text input | | value | string | null | Value for the text input | | valid | boolean | false | Whether the value is valid or not | | message | string | null | Validation message to be shown | | style | style | {} | Style changes to the main ScrollView | | iconStyle | style | {} | Style changes to the icon View | -| titleStyle | style | {} | Style changes to the title Text | +| labelStyle | style | {} | Style changes to the label Text | | inputStyle | style | {} | Style changes to the TextInput | | messageStyle | style | {} | Style changes to the validation message Text | | icon | element | null | Any react component to be used as icon | @@ -542,33 +543,33 @@ A wrapper that will manage auto-focusing and auto-scrolling for its children wid \+ Any other [TextInput](https://facebook.github.io/react-native/docs/textinput.html#content) prop you wish to pass. -#### Other widgets +#### Other components -My intention is to implement most of [FaridSafi/react-native-gifted-form](https://github.com/FaridSafi/react-native-gifted-form)'s widgets. But I'll do each one only when I need it in a real project, so it might take some time. +My intention is to implement most of [FaridSafi/react-native-gifted-form](https://github.com/FaridSafi/react-native-gifted-form)'s components. But I'll do each one only when I need it in a real project, so it might take some time. PR's are very much welcome! -## Creating new widgets +## Creating new components -Any react component can be rendered inside Stateless Form as a widget. But there is a special case below: +Any react component can be rendered inside Stateless Form as a component. But there is a special case below: -#### Focusable input widgets +#### Focusable input components -If you want your widget to receive focus when previous widget finished editing, you must implement the following pattern: +If you want your component to receive focus when previous component finished editing, you must implement the following pattern: -- 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. -- Your widget must have `valid` and `value` on its `propTypes`. This is how `StatelessForm` will recognize it as a focusable and/or scrollable input widget. It is important that only focusable or scrollable widgets have these props on `propTypes`. +- Your component should implement the `focus()` method. +- Your component should implement the `blur()` method. +- Your component 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. +- Your component must have `valid` and `value` on its `propTypes`. This is how `StatelessForm` will recognize it as a focusable and/or scrollable input component. It is important that only focusable or scrollable components have these props on `propTypes`. -#### Scrollable input widgets +#### Scrollable input components -If you want your widget to receive scroll when showing keyboard, you must implement the following pattern: +If you want your component to receive scroll when showing keyboard, you must implement the following pattern: -- 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. -- Your widget should implement `onBlur` and call `this.props.onBlur` on blur. -- Your widget also must have `valid` and `value` on its `propTypes`. +- Your component should implement `onFocus` and call `this.props.onFocus(scrollTo)` on focus. `scrollTo` must be your component's `y` position. +- You can get your `y` position using `onLayout` prop. Check [InlineTextInput](https://github.com/danielweinmann/react-native-stateless-form/blob/master/components/InlineTextInput.js) for references on how to implement it. +- Your component should implement `onBlur` and call `this.props.onBlur` on blur. +- Your component also must have `valid` and `value` on its `propTypes`. ## Contributing diff --git a/StatelessForm.js b/StatelessForm.js index adba039..eedb548 100644 --- a/StatelessForm.js +++ b/StatelessForm.js @@ -1,4 +1,6 @@ -import React, { Platform, Component, PropTypes, ScrollView, View } from 'react-native' +import React, { Component } from 'react' +import { Platform, ScrollView, View } from 'react-native' +import PropTypes from 'prop-types' export default class StatelessForm extends Component { componentDidMount() { @@ -6,6 +8,8 @@ export default class StatelessForm extends Component { } childrenWithProps() { + if (!this.props.children) return + let nextInput = null let inputCount = 0 return React.Children.map(this.props.children, (child) => child).reverse().map((child) => { @@ -51,7 +55,7 @@ export default class StatelessForm extends Component { render() { return ( } ) - } + } } StatelessForm.propTypes = { style: PropTypes.oneOfType([ - React.PropTypes.object, - React.PropTypes.arrayOf(React.PropTypes.object), + PropTypes.object, + PropTypes.arrayOf(PropTypes.object), ]), } diff --git a/widgets/InlineTextInput.js b/components/InlineTextInput.js similarity index 73% rename from widgets/InlineTextInput.js rename to components/InlineTextInput.js index 2d7aabe..b86a724 100644 --- a/widgets/InlineTextInput.js +++ b/components/InlineTextInput.js @@ -1,4 +1,6 @@ -import React, { Component, PropTypes, View, Text, TextInput } from 'react-native' +import React, { Component } from 'react' +import { View, Text, TextInput, StyleSheet } from 'react-native' +import PropTypes from 'prop-types' export default class InlineTextInput extends Component { componentDidMount() { @@ -55,22 +57,28 @@ export default class InlineTextInput extends Component { renderMessage() { const { message, messageStyle } = this.props + const style = StyleSheet.flatten(this.props.style) if (this.shouldDisplayMessage()) { return( - - { message } - + + + { message } + + ) } } render() { - const { title, value, style, titleStyle, inputStyle, nextInput, onBlur, multiline } = this.props + const { label, value, labelStyle, inputStyle, nextInput, onBlur, multiline } = this.props + const style = StyleSheet.flatten(this.props.style) return ( { this.renderIcon() } - {title} + {label} 0 && !valid && message) + } + + handleSubmitEditing() { + const { nextInput, onNextInputFocus } = this.props + onNextInputFocus && onNextInputFocus(nextInput, this) + } + + renderIcon() { + const { icon, validIcon, invalidIcon, valid, value, iconStyle } = this.props + if (!icon) + return + let renderedIcon = null + if (value && value.length > 0) { + renderedIcon = (valid ? (validIcon ? validIcon : icon) : (invalidIcon ? invalidIcon : icon)) + } else { + renderedIcon = icon + } + return ( + + {renderedIcon} + + ) + } + + renderMessage() { + const { message, messageStyle } = this.props + const style = StyleSheet.flatten(this.props.style) + if (this.shouldDisplayMessage()) { + return( + + + { message } + + + ) + } + } + + render() { + const { label, value, labelStyle, inputStyle, nextInput, onBlur, multiline } = this.props + const style = StyleSheet.flatten(this.props.style) + return ( + + + {label} + + + { this.renderIcon() } + + + { this.renderMessage() } + + ) + } +} + +const stylePropType = PropTypes.oneOfType([ + PropTypes.object, + PropTypes.arrayOf(PropTypes.object), +]) + +LabeledTextInput.propTypes = { + label: PropTypes.string, + value: PropTypes.string, + valid: PropTypes.bool, + message: PropTypes.string, + style: stylePropType, + iconStyle: stylePropType, + labelStyle: stylePropType, + inputStyle: stylePropType, + messageStyle: stylePropType, + icon: PropTypes.element, + validIcon: PropTypes.element, + invalidIcon: PropTypes.element, +} + +LabeledTextInput.defaultProps = { + label: 'Use label prop', + value: null, + valid: false, + message: null, + style: {}, + iconStyle: {}, + labelStyle: {}, + inputStyle: {}, + messageStyle: {}, + icon: null, + validIcon: null, + invalidIcon: null, +} diff --git a/index.js b/index.js index 74de23b..0b4b8cb 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ import StatelessForm from './StatelessForm' -import InlineTextInput from './widgets/InlineTextInput' +import LabeledTextInput from './components/LabeledTextInput' +import InlineTextInput from './components/InlineTextInput' -module.exports = { StatelessForm, InlineTextInput } +module.exports = { StatelessForm, LabeledTextInput, InlineTextInput } diff --git a/package.json b/package.json index 44916c9..613dbff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-stateless-form", - "version": "0.2.0", + "version": "0.3.1", "description": "Stateless presentational form components for React Native", "main": "index.js", "scripts": { @@ -26,5 +26,8 @@ "bugs": { "url": "https://github.com/danielweinmann/react-native-stateless-form/issues" }, - "homepage": "https://github.com/danielweinmann/react-native-stateless-form#readme" + "homepage": "https://github.com/danielweinmann/react-native-stateless-form#readme", + "dependencies": { + "prop-types": "^15.6.0" + } }