├── StatelessForm.gif ├── index.js ├── package.json ├── LICENSE ├── StatelessForm.js ├── components ├── InlineTextInput.js └── LabeledTextInput.js └── README.md /StatelessForm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielweinmann/react-native-stateless-form/HEAD/StatelessForm.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import StatelessForm from './StatelessForm' 2 | import LabeledTextInput from './components/LabeledTextInput' 3 | import InlineTextInput from './components/InlineTextInput' 4 | 5 | module.exports = { StatelessForm, LabeledTextInput, InlineTextInput } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-stateless-form", 3 | "version": "0.3.1", 4 | "description": "Stateless presentational form components for React Native", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/danielweinmann/react-native-stateless-form.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "native", 16 | "react native", 17 | "form", 18 | "stateless", 19 | "presentational", 20 | "components", 21 | "ios", 22 | "android" 23 | ], 24 | "author": "Daniel Weinmann", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/danielweinmann/react-native-stateless-form/issues" 28 | }, 29 | "homepage": "https://github.com/danielweinmann/react-native-stateless-form#readme", 30 | "dependencies": { 31 | "prop-types": "^15.6.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daniel Weinmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /StatelessForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Platform, ScrollView, View } from 'react-native' 3 | import PropTypes from 'prop-types' 4 | 5 | export default class StatelessForm extends Component { 6 | componentDidMount() { 7 | this.willFocusInput = false 8 | } 9 | 10 | childrenWithProps() { 11 | if (!this.props.children) return 12 | 13 | let nextInput = null 14 | let inputCount = 0 15 | return React.Children.map(this.props.children, (child) => child).reverse().map((child) => { 16 | if (child.type.propTypes && child.type.propTypes.value && child.type.propTypes.valid) { 17 | inputCount++ 18 | const input = React.cloneElement(child, { 19 | ref: `input${inputCount}`, 20 | nextInput: nextInput, 21 | onNextInputFocus: this.handleNextInputFocus.bind(this), 22 | onFocus: this.handleFocus.bind(this), 23 | onBlur: this.handleBlur.bind(this), 24 | }) 25 | nextInput = input 26 | return input 27 | } else { 28 | return child 29 | } 30 | }).reverse() 31 | } 32 | 33 | handleNextInputFocus(nextInput, currentInput) { 34 | if (nextInput) { 35 | const input = this.refs[nextInput.ref] 36 | this.willFocusInput = true 37 | input.focus() 38 | } else { 39 | currentInput.blur() 40 | } 41 | } 42 | 43 | handleBlur() { 44 | if (!this.willFocusInput) { 45 | this.refs.scrollView.scrollTo({y: 0}) 46 | } 47 | this.willFocusInput = false 48 | } 49 | 50 | handleFocus(scrollTo) { 51 | this.willFocusInput = false 52 | this.refs.scrollView.scrollTo({y: scrollTo}) 53 | } 54 | 55 | render() { 56 | return ( 57 | 66 | {this.childrenWithProps()} 67 | { Platform.OS == 'android' && } 68 | 69 | ) 70 | } 71 | } 72 | 73 | StatelessForm.propTypes = { 74 | style: PropTypes.oneOfType([ 75 | PropTypes.object, 76 | PropTypes.arrayOf(PropTypes.object), 77 | ]), 78 | } 79 | 80 | StatelessForm.defaultProps = { 81 | style: {}, 82 | } 83 | -------------------------------------------------------------------------------- /components/InlineTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text, TextInput, StyleSheet } from 'react-native' 3 | import PropTypes from 'prop-types' 4 | 5 | export default class InlineTextInput extends Component { 6 | componentDidMount() { 7 | this.scrollTo = 0 8 | } 9 | 10 | handleLayout(event) { 11 | this.scrollTo = event.nativeEvent.layout.y 12 | } 13 | 14 | handleFocus() { 15 | const { onFocus } = this.props 16 | onFocus && onFocus(this.scrollTo) 17 | } 18 | 19 | focus() { 20 | this.refs.input.focus() 21 | } 22 | 23 | blur() { 24 | this.refs.input.blur() 25 | } 26 | 27 | shouldDisplayMessage() { 28 | const { value, valid, message } = this.props 29 | return (value && value.length > 0 && !valid && message) 30 | } 31 | 32 | handleSubmitEditing() { 33 | const { nextInput, onNextInputFocus } = this.props 34 | onNextInputFocus && onNextInputFocus(nextInput, this) 35 | } 36 | 37 | renderIcon() { 38 | const { icon, validIcon, invalidIcon, valid, value, iconStyle } = this.props 39 | if (!icon) 40 | return 41 | let renderedIcon = null 42 | if (value && value.length > 0) { 43 | renderedIcon = (valid ? (validIcon ? validIcon : icon) : (invalidIcon ? invalidIcon : icon)) 44 | } else { 45 | renderedIcon = icon 46 | } 47 | return ( 48 | 53 | {renderedIcon} 54 | 55 | ) 56 | } 57 | 58 | renderMessage() { 59 | const { message, messageStyle } = this.props 60 | const style = StyleSheet.flatten(this.props.style) 61 | if (this.shouldDisplayMessage()) { 62 | return( 63 | 66 | 72 | { message } 73 | 74 | 75 | ) 76 | } 77 | } 78 | 79 | render() { 80 | const { label, value, labelStyle, inputStyle, nextInput, onBlur, multiline } = this.props 81 | const style = StyleSheet.flatten(this.props.style) 82 | return ( 83 | 92 | 99 | { this.renderIcon() } 100 | 108 | {label} 109 | 110 | 127 | 128 | { this.renderMessage() } 129 | 130 | ) 131 | } 132 | } 133 | 134 | const stylePropType = PropTypes.oneOfType([ 135 | PropTypes.object, 136 | PropTypes.arrayOf(PropTypes.object), 137 | ]) 138 | 139 | InlineTextInput.propTypes = { 140 | label: PropTypes.string, 141 | value: PropTypes.string, 142 | valid: PropTypes.bool, 143 | message: PropTypes.string, 144 | style: stylePropType, 145 | iconStyle: stylePropType, 146 | labelStyle: stylePropType, 147 | inputStyle: stylePropType, 148 | messageStyle: stylePropType, 149 | icon: PropTypes.element, 150 | validIcon: PropTypes.element, 151 | invalidIcon: PropTypes.element, 152 | } 153 | 154 | InlineTextInput.defaultProps = { 155 | label: 'Use label prop', 156 | value: null, 157 | valid: false, 158 | message: null, 159 | style: {}, 160 | iconStyle: {}, 161 | labelStyle: {}, 162 | inputStyle: {}, 163 | messageStyle: {}, 164 | icon: null, 165 | validIcon: null, 166 | invalidIcon: null, 167 | } 168 | -------------------------------------------------------------------------------- /components/LabeledTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, Text, TextInput, StyleSheet } from 'react-native' 3 | import PropTypes from 'prop-types' 4 | 5 | export default class LabeledTextInput extends Component { 6 | componentDidMount() { 7 | this.scrollTo = 0 8 | } 9 | 10 | handleLayout(event) { 11 | this.scrollTo = event.nativeEvent.layout.y 12 | } 13 | 14 | handleFocus() { 15 | const { onFocus } = this.props 16 | onFocus && onFocus(this.scrollTo) 17 | } 18 | 19 | focus() { 20 | this.refs.input.focus() 21 | } 22 | 23 | blur() { 24 | this.refs.input.blur() 25 | } 26 | 27 | shouldDisplayMessage() { 28 | const { value, valid, message } = this.props 29 | return (value && value.length > 0 && !valid && message) 30 | } 31 | 32 | handleSubmitEditing() { 33 | const { nextInput, onNextInputFocus } = this.props 34 | onNextInputFocus && onNextInputFocus(nextInput, this) 35 | } 36 | 37 | renderIcon() { 38 | const { icon, validIcon, invalidIcon, valid, value, iconStyle } = this.props 39 | if (!icon) 40 | return 41 | let renderedIcon = null 42 | if (value && value.length > 0) { 43 | renderedIcon = (valid ? (validIcon ? validIcon : icon) : (invalidIcon ? invalidIcon : icon)) 44 | } else { 45 | renderedIcon = icon 46 | } 47 | return ( 48 | 51 | {renderedIcon} 52 | 53 | ) 54 | } 55 | 56 | renderMessage() { 57 | const { message, messageStyle } = this.props 58 | const style = StyleSheet.flatten(this.props.style) 59 | if (this.shouldDisplayMessage()) { 60 | return( 61 | 64 | 69 | { message } 70 | 71 | 72 | ) 73 | } 74 | } 75 | 76 | render() { 77 | const { label, value, labelStyle, inputStyle, nextInput, onBlur, multiline } = this.props 78 | const style = StyleSheet.flatten(this.props.style) 79 | return ( 80 | 92 | 99 | {label} 100 | 101 | 102 | { this.renderIcon() } 103 | 124 | 125 | { this.renderMessage() } 126 | 127 | ) 128 | } 129 | } 130 | 131 | const stylePropType = PropTypes.oneOfType([ 132 | PropTypes.object, 133 | PropTypes.arrayOf(PropTypes.object), 134 | ]) 135 | 136 | LabeledTextInput.propTypes = { 137 | label: PropTypes.string, 138 | value: PropTypes.string, 139 | valid: PropTypes.bool, 140 | message: PropTypes.string, 141 | style: stylePropType, 142 | iconStyle: stylePropType, 143 | labelStyle: stylePropType, 144 | inputStyle: stylePropType, 145 | messageStyle: stylePropType, 146 | icon: PropTypes.element, 147 | validIcon: PropTypes.element, 148 | invalidIcon: PropTypes.element, 149 | } 150 | 151 | LabeledTextInput.defaultProps = { 152 | label: 'Use label prop', 153 | value: null, 154 | valid: false, 155 | message: null, 156 | style: {}, 157 | iconStyle: {}, 158 | labelStyle: {}, 159 | inputStyle: {}, 160 | messageStyle: {}, 161 | icon: null, 162 | validIcon: null, 163 | invalidIcon: null, 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-stateless-form 2 | 3 | *

Never again worry about scrolling and focusing form fields

4 | *

Display icons and inline error messages with ease

5 | *

Use any form state management tool you want

6 | 7 | ## Screen capture 8 | 9 | ![](https://raw.githubusercontent.com/danielweinmann/react-native-stateless-form/master/StatelessForm.gif) 10 | 11 | ## What it does 12 | 13 | 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. 14 | 15 | - It uses inline form fields with icons and labels 16 | - It displays different icons for valid and invalid field values 17 | - It displays validation message inside the field 18 | - When a field receives focus, it displays a keyboard (\*) 19 | - If it is not the last field in the form, the keyboard return key is set to `Next` 20 | - If it is the last field in the form, the keyboard return key is set to `Done` and hides keaboard on return 21 | - When a field receives focus, the form scrolls to the top of the field to avoid it being hidden behind the keyboard 22 | - When all fields lose focus, the form scrolls back to the top of the form 23 | 24 | (\*) Unless an external keyboard is connected to the device 25 | 26 | ## What it does NOT do 27 | 28 | - 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. 29 | - It does not implement form state management. We recommend using [Redux Form](http://erikras.github.io/redux-form/) for that. But you can use anything you want. 30 | - It does not implement a submit button and enabled/disabled/loading behaviour for you. We recommend using [apsl-react-native-button](https://github.com/APSL/react-native-button) for that. But you can use anything you want. 31 | 32 | ## Support 33 | 34 | - React Native 0.25+ 35 | - iOS 36 | - Android (see installation below) 37 | 38 | ## Inspiration 39 | 40 | This package is inspired by [FaridSafi/react-native-gifted-form](https://github.com/FaridSafi/react-native-gifted-form), and my intention is to merge with it in the future. 41 | 42 | The reason for creating a new package is that I want the form components to be presentational only, and not to store state at all. This way we can easily integrate with Redux Form, any other form management tool, or even implement our own form management. 43 | 44 | ## Installation 45 | 46 | ```npm install react-native-stateless-form --save``` 47 | 48 | #### Android 49 | 50 | You should add `android:windowSoftInputMode="adjustNothing"` attribute to the `` tag with `android:name=".MainActivity"` in your `AndroidManifest.xml`. Otherwise, it will have duplicate scroll behaviour. 51 | 52 | ## Examples 53 | 54 | #### The dirtiest example using React state 55 | 56 | ```js 57 | import React, { Component } from 'react-native' 58 | import Icon from 'react-native-vector-icons/MaterialIcons' 59 | import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' 60 | 61 | class Form extends Component { 62 | constructor(props, context) { 63 | super(props, context) 64 | this.state = { 65 | name: null, 66 | email: null, 67 | password: null, 68 | } 69 | } 70 | 71 | render() { 72 | const { name, email, password } = this.state 73 | const nameValid = (name && name.length > 0 ? true : false) 74 | const emailValid = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email) 75 | const passwordValid = (password && password.length >= 8 ? true : false) 76 | return ( 77 | 82 | } 90 | validIcon={ } 91 | invalidIcon={ } 92 | value={name} 93 | valid={nameValid} 94 | message={name && !nameValid ? 'Please fill your name' : null} 95 | onChangeText={(text) => { this.setState({name: text}) }} 96 | /> 97 | } 108 | validIcon={ } 109 | invalidIcon={ } 110 | value={email} 111 | valid={emailValid} 112 | message={email && !emailValid ? 'Please enter a valid email address' : null} 113 | onChangeText={(text) => { this.setState({email: text}) }} 114 | /> 115 | } 126 | validIcon={ } 127 | invalidIcon={ } 128 | value={password} 129 | valid={passwordValid} 130 | message={password && !passwordValid ? 'Password too short' : null} 131 | onChangeText={(text) => { this.setState({password: text}) }} 132 | /> 133 | 134 | ) 135 | } 136 | } 137 | 138 | import { AppRegistry } from 'react-native' 139 | AppRegistry.registerComponent('Form', () => Form) 140 | ``` 141 | 142 | #### Create your own component to keep it DRY 143 | 144 | ```js 145 | import React, { Component } from 'react-native' 146 | import PropTypes from 'prop-types' 147 | import Icon from 'react-native-vector-icons/MaterialIcons' 148 | import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' 149 | 150 | class FormInput extends Component { 151 | // You MUST implement focus and blur methods for your component to work 152 | focus() { 153 | this.refs.input.focus() 154 | } 155 | 156 | blur() { 157 | this.refs.input.blur() 158 | } 159 | 160 | render() { 161 | const { iconName } = this.props 162 | return ( 163 | } 170 | validIcon={ } 171 | invalidIcon={ } 172 | { ...this.props } 173 | /> 174 | ) 175 | } 176 | } 177 | 178 | // You MUST add these two props to propTypes in order to have auto-focus and auto-scroll working 179 | FormInput.propTypes = { 180 | value: PropTypes.string, 181 | valid: PropTypes.bool, 182 | } 183 | 184 | class Form extends Component { 185 | constructor(props, context) { 186 | super(props, context) 187 | this.state = { 188 | name: null, 189 | email: null, 190 | password: null, 191 | } 192 | } 193 | 194 | render() { 195 | const { name, email, password } = this.state 196 | const nameValid = (name && name.length > 0 ? true : false) 197 | const emailValid = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email) 198 | const passwordValid = (password && password.length >= 8 ? true : false) 199 | return ( 200 | 201 | { this.setState({name: text}) }} 209 | /> 210 | { this.setState({email: text}) }} 221 | /> 222 | { this.setState({password: text}) }} 233 | /> 234 | 235 | ) 236 | } 237 | } 238 | 239 | import { AppRegistry } from 'react-native' 240 | AppRegistry.registerComponent('Form', () => Form) 241 | ``` 242 | 243 | #### Usage with validate-model 244 | 245 | ```js 246 | import React, { Component } from 'react-native' 247 | import PropTypes from 'prop-types' 248 | import Icon from 'react-native-vector-icons/MaterialIcons' 249 | import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' 250 | import { validate } from 'validate-model' 251 | 252 | const UserValidators = { 253 | name: { 254 | title: 'Name', 255 | validate: [{ 256 | validator: 'isLength', 257 | arguments: [1, 255], 258 | }] 259 | }, 260 | email: { 261 | title: 'Email', 262 | validate: [{ 263 | validator: 'isLength', 264 | arguments: [1, 255], 265 | }, 266 | { 267 | validator: 'isEmail', 268 | message: '{TITLE} must be valid', 269 | }] 270 | }, 271 | password: { 272 | title: 'Password', 273 | validate: [{ 274 | validator: 'isLength', 275 | arguments: [8, 255], 276 | message: '{TITLE} is too short', 277 | }] 278 | }, 279 | } 280 | 281 | class FormInput extends Component { 282 | focus() { 283 | this.refs.input.focus() 284 | } 285 | 286 | blur() { 287 | this.refs.input.blur() 288 | } 289 | 290 | render() { 291 | const { iconName, name, value } = this.props 292 | const { valid, messages } = validate(UserValidators[name], value) 293 | const message = (messages && messages.lenght > 0 ? messages[0] : null) 294 | return ( 295 | } 302 | validIcon={ } 303 | invalidIcon={ } 304 | valid={valid} 305 | message={message} 306 | { ...this.props } 307 | /> 308 | ) 309 | } 310 | } 311 | 312 | FormInput.propTypes = { 313 | value: PropTypes.string, 314 | valid: PropTypes.bool, 315 | } 316 | 317 | class Form extends Component { 318 | constructor(props, context) { 319 | super(props, context) 320 | this.state = { 321 | name: null, 322 | email: null, 323 | password: null, 324 | } 325 | } 326 | 327 | render() { 328 | const { name, email, password } = this.state 329 | return ( 330 | 331 | { this.setState({name: text}) }} 338 | /> 339 | { this.setState({email: text}) }} 349 | /> 350 | { this.setState({password: text}) }} 360 | /> 361 | 362 | ) 363 | } 364 | } 365 | 366 | import { AppRegistry } from 'react-native' 367 | AppRegistry.registerComponent('Form', () => Form) 368 | ``` 369 | 370 | #### Usage with Redux Form 371 | 372 | ```js 373 | import React, { Component } from 'react-native' 374 | import PropTypes from 'prop-types' 375 | import Icon from 'react-native-vector-icons/MaterialIcons' 376 | import { StatelessForm, InlineTextInput } from 'react-native-stateless-form' 377 | import { validateAll } from 'validate-model' 378 | import { Provider } from 'react-redux' 379 | import { createStore, combineReducers, applyMiddleware } from 'redux' 380 | import { reduxForm, reducer as formReducer } from 'redux-form' 381 | import createLogger from 'redux-logger' 382 | 383 | const UserValidators = { 384 | name: { 385 | title: 'Name', 386 | validate: [{ 387 | validator: 'isLength', 388 | arguments: [1, 255], 389 | }] 390 | }, 391 | email: { 392 | title: 'Email', 393 | validate: [{ 394 | validator: 'isLength', 395 | arguments: [1, 255], 396 | }, 397 | { 398 | validator: 'isEmail', 399 | message: '{TITLE} must be valid', 400 | }] 401 | }, 402 | password: { 403 | title: 'Password', 404 | validate: [{ 405 | validator: 'isLength', 406 | arguments: [8, 255], 407 | message: '{TITLE} is too short', 408 | }] 409 | }, 410 | } 411 | 412 | const validate = values => { 413 | const validation = validateAll(UserValidators, values) 414 | if (!validation.valid) return validation.messages 415 | return {} 416 | } 417 | 418 | class FormInput extends Component { 419 | focus() { 420 | this.refs.input.focus() 421 | } 422 | 423 | blur() { 424 | this.refs.input.blur() 425 | } 426 | 427 | render() { 428 | const { iconName, name, value, error } = this.props 429 | const message = ( error && error.length > 0 ? error[0] : null) 430 | return ( 431 | } 438 | validIcon={ } 439 | invalidIcon={ } 440 | message={message} 441 | { ...this.props } 442 | /> 443 | ) 444 | } 445 | } 446 | 447 | FormInput.propTypes = { 448 | value: PropTypes.string, 449 | valid: PropTypes.bool, 450 | } 451 | 452 | class Form extends Component { 453 | render() { 454 | const { fields: { name, email, password } } = this.props 455 | return ( 456 | 457 | 464 | 474 | 484 | 485 | ) 486 | } 487 | } 488 | 489 | Form = reduxForm({ 490 | form: 'user', 491 | fields: ['name', 'email', 'password'], 492 | validate 493 | })(Form); 494 | 495 | const reducers = { 496 | form: formReducer 497 | } 498 | const reducer = combineReducers(reducers) 499 | const createStoreWithMiddleware = applyMiddleware(createLogger())(createStore) 500 | function configureStore(initialState) { 501 | return createStoreWithMiddleware(reducer, initialState) 502 | } 503 | const store = configureStore() 504 | 505 | const Root = () => ( 506 | 507 |
508 | 509 | ) 510 | 511 | import { AppRegistry } from 'react-native' 512 | AppRegistry.registerComponent('Form', () => Root) 513 | ``` 514 | 515 | ## StatelessForm 516 | 517 | A wrapper that will manage auto-focusing and auto-scrolling for its children components 518 | 519 | | Property | Type | Default | Description | 520 | |---------------|----------|--------------|----------------------------------------------------------------| 521 | | style | style | {} | Style for the form wrapper | 522 | 523 | \+ Any other [ScrollView](https://facebook.github.io/react-native/docs/scrollview.html#content) prop you wish to pass. 524 | 525 | ## Components 526 | 527 | #### InlineTextInput 528 | 529 | | Property | Type | Default | Description | 530 | |---------------|----------|--------------|----------------------------------------------------------------| 531 | | label | string | 'Use label prop' | Label for the text input | 532 | | value | string | null | Value for the text input | 533 | | valid | boolean | false | Whether the value is valid or not | 534 | | message | string | null | Validation message to be shown | 535 | | style | style | {} | Style changes to the main ScrollView | 536 | | iconStyle | style | {} | Style changes to the icon View | 537 | | labelStyle | style | {} | Style changes to the label Text | 538 | | inputStyle | style | {} | Style changes to the TextInput | 539 | | messageStyle | style | {} | Style changes to the validation message Text | 540 | | icon | element | null | Any react component to be used as icon | 541 | | validIcon | element | null | Any react component to be used as icon when valid. Requires `icon` prop | 542 | | invalidIcon | element | null | Any react component to be used as icon when invalid. Requires `icon` prop | 543 | 544 | \+ Any other [TextInput](https://facebook.github.io/react-native/docs/textinput.html#content) prop you wish to pass. 545 | 546 | #### Other components 547 | 548 | 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. 549 | 550 | PR's are very much welcome! 551 | 552 | ## Creating new components 553 | 554 | Any react component can be rendered inside Stateless Form as a component. But there is a special case below: 555 | 556 | #### Focusable input components 557 | 558 | If you want your component to receive focus when previous component finished editing, you must implement the following pattern: 559 | 560 | - Your component should implement the `focus()` method. 561 | - Your component should implement the `blur()` method. 562 | - 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. 563 | - 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`. 564 | 565 | #### Scrollable input components 566 | 567 | If you want your component to receive scroll when showing keyboard, you must implement the following pattern: 568 | 569 | - Your component should implement `onFocus` and call `this.props.onFocus(scrollTo)` on focus. `scrollTo` must be your component's `y` position. 570 | - 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. 571 | - Your component should implement `onBlur` and call `this.props.onBlur` on blur. 572 | - Your component also must have `valid` and `value` on its `propTypes`. 573 | 574 | ## Contributing 575 | 576 | Please create issues and send pull requests! 577 | 578 | ## License 579 | 580 | [MIT](LICENSE) 581 | --------------------------------------------------------------------------------