├── .gitignore ├── LICENSE ├── README.md ├── demoApp ├── .buckconfig ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── App.js ├── __tests__ │ └── App-test.js ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── demoapp │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── components │ └── FormInput.js ├── index.js ├── ios │ ├── Podfile │ ├── Podfile.lock │ ├── demoApp-tvOS │ │ └── Info.plist │ ├── demoApp-tvOSTests │ │ └── Info.plist │ ├── demoApp.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── demoApp-tvOS.xcscheme │ │ │ └── demoApp.xcscheme │ ├── demoApp.xcworkspace │ │ └── contents.xcworkspacedata │ ├── demoApp │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ └── demoAppTests │ │ ├── Info.plist │ │ └── demoAppTests.m ├── metro.config.js ├── package.json ├── react-native-form-helpers │ ├── dictionary.js │ └── index.js └── yarn.lock ├── package.json ├── src └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) LawnStarter 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-form-helpers 2 | [![npm version](https://badge.fury.io/js/react-native-form-helpers.svg)](https://badge.fury.io/js/react-native-form-helpers) 3 | 4 | **Step 1:**
5 | `npm install react-native-form-helpers` 6 | 7 | **Step 2:**
8 | Create a dictionary file ([example dictionary](demoApp/react-native-form-helpers/dictionary.js)) 9 | 10 | **Step 3:**
11 | 12 | ```js 13 | import RNFormHelpers from "./react-native-form-helpers"; 14 | import { validationDictionary } from "./dictionary.js"; // location of your dictionary file 15 | 16 | export const validationService = RNFormHelpers({ 17 | dictionary: validationDictionary 18 | }); 19 | ``` 20 | 21 | **Step 4:**
22 | Import into your form and utilize the built-in methods. See below tutorial or [sample app](demoApp/App.js) for more details. 23 | 24 | ## Tutorial Series: 25 | 26 | https://medium.com/lawnstarter-engineering/how-to-create-custom-forms-with-validation-and-scroll-to-invalid-logic-in-react-native-part-one-43e5f7cdf807 27 | 28 | https://medium.com/lawnstarter-engineering/how-to-create-custom-forms-with-validation-and-scroll-to-invalid-logic-in-react-native-part-two-9834849d4d78 29 | 30 | https://medium.com/lawnstarter-engineering/how-to-create-custom-forms-with-validation-and-scroll-to-invalid-logic-in-react-native-f6a4cc049095 31 | -------------------------------------------------------------------------------- /demoApp/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /demoApp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /demoApp/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | node_modules/react-native/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | node_modules/react-native/Libraries/polyfills/.* 18 | 19 | ; These should not be required directly 20 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 21 | node_modules/warning/.* 22 | 23 | ; Flow doesn't support platforms 24 | .*/Libraries/Utilities/HMRLoadingView.js 25 | 26 | [untyped] 27 | .*/node_modules/@react-native-community/cli/.*/.* 28 | 29 | [include] 30 | 31 | [libs] 32 | node_modules/react-native/Libraries/react-native/react-native-interface.js 33 | node_modules/react-native/flow/ 34 | 35 | [options] 36 | emoji=true 37 | 38 | esproposal.optional_chaining=enable 39 | esproposal.nullish_coalescing=enable 40 | 41 | module.file_ext=.js 42 | module.file_ext=.json 43 | module.file_ext=.ios.js 44 | 45 | module.system=haste 46 | module.system.haste.use_name_reducers=true 47 | # get basename 48 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 49 | # strip .js or .js.flow suffix 50 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 51 | # strip .ios suffix 52 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 53 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 54 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 55 | module.system.haste.paths.blacklist=.*/__tests__/.* 56 | module.system.haste.paths.blacklist=.*/__mocks__/.* 57 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 58 | module.system.haste.paths.whitelist=/node_modules/react-native/RNTester/.* 59 | module.system.haste.paths.whitelist=/node_modules/react-native/IntegrationTests/.* 60 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/react-native/react-native-implementation.js 61 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 62 | 63 | munge_underscores=true 64 | 65 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 66 | 67 | suppress_type=$FlowIssue 68 | suppress_type=$FlowFixMe 69 | suppress_type=$FlowFixMeProps 70 | suppress_type=$FlowFixMeState 71 | 72 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 73 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 74 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 75 | 76 | [lints] 77 | sketchy-null-number=warn 78 | sketchy-null-mixed=warn 79 | sketchy-number=warn 80 | untyped-type-import=warn 81 | nonstrict-import=warn 82 | deprecated-type=warn 83 | unsafe-getters-setters=warn 84 | inexact-spread=warn 85 | unnecessary-invariant=warn 86 | signature-verification-failure=warn 87 | deprecated-utility=error 88 | 89 | [strict] 90 | deprecated-type 91 | nonstrict-import 92 | sketchy-null 93 | unclear-type 94 | unsafe-getters-setters 95 | untyped-import 96 | untyped-type-import 97 | 98 | [version] 99 | ^0.98.0 100 | -------------------------------------------------------------------------------- /demoApp/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /demoApp/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /demoApp/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /demoApp/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /demoApp/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component, Fragment} from 'react'; 2 | import { 3 | Button, 4 | View, 5 | StyleSheet, 6 | Text, 7 | ScrollView, 8 | KeyboardAvoidingView, 9 | Switch, 10 | } from 'react-native'; 11 | import {validationService} from './index'; 12 | import FormInput from './components/FormInput'; 13 | 14 | export default class App extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | inputs: { 19 | first_name: { 20 | type: 'generic', 21 | value: '', 22 | }, 23 | last_name: { 24 | type: 'generic', 25 | value: '', 26 | }, 27 | birthday_month: { 28 | type: 'month', 29 | value: '', 30 | }, 31 | birthday_day: { 32 | type: 'day', 33 | value: '', 34 | }, 35 | birthday_year: { 36 | type: 'year', 37 | value: '', 38 | }, 39 | state: { 40 | type: 'state', 41 | value: '', 42 | }, 43 | zip: { 44 | type: 'zip', 45 | value: '', 46 | }, 47 | tos: { 48 | type: 'bool', 49 | value: false, 50 | }, 51 | }, 52 | }; 53 | 54 | this.onInputChange = validationService.onInputChange.bind(this); 55 | this.getFormValidation = validationService.getFormValidation.bind(this); 56 | this.setInputPosition = validationService.setInputPosition.bind(this); 57 | this.submit = this.submit.bind(this); 58 | 59 | this.scrollView = React.createRef(); 60 | } 61 | 62 | submit() { 63 | const firstInvalidCoordinate = this.getFormValidation(); 64 | 65 | if (firstInvalidCoordinate !== null) { 66 | this.scrollView.current.scrollTo({ 67 | x: 0, 68 | y: firstInvalidCoordinate, 69 | animated: true, 70 | }); 71 | return; 72 | } 73 | 74 | // if we make it to this point, we can actually submit the form 75 | } 76 | 77 | renderError(id) { 78 | const {inputs} = this.state; 79 | if (inputs[id].errorLabel) { 80 | return {inputs[id].errorLabel}; 81 | } 82 | return null; 83 | } 84 | 85 | render() { 86 | const {inputs} = this.state; 87 | 88 | return ( 89 | 90 | 91 | { 94 | this.onInputChange({id: 'first_name', value}); 95 | }} 96 | errorLabel={inputs.first_name.errorLabel} 97 | touched={inputs.first_name.touched} 98 | onLayout={({nativeEvent}) => { 99 | this.setInputPosition({ 100 | ids: ['first_name'], 101 | value: nativeEvent.layout.y, 102 | }); 103 | }} 104 | /> 105 | 106 | { 109 | this.onInputChange({id: 'last_name', value}); 110 | }} 111 | errorLabel={inputs.last_name.errorLabel} 112 | touched={inputs.last_name.touched} 113 | onLayout={({nativeEvent}) => { 114 | this.setInputPosition({ 115 | ids: ['last_name'], 116 | value: nativeEvent.layout.y, 117 | }); 118 | }} 119 | /> 120 | 121 | 122 | Birthday? 123 | { 125 | this.setInputPosition({ 126 | ids: ['birthday_month', 'birthday_day'], 127 | value: nativeEvent.layout.y, 128 | }); 129 | }} 130 | style={styles.split}> 131 | 132 | { 134 | this.onInputChange({id: 'birthday_month', value}); 135 | }} 136 | errorLabel={inputs.birthday_month.errorLabel} 137 | touched={inputs.birthday_month.touched} 138 | placeholder="Month" 139 | keyboardType="number-pad" 140 | /> 141 | 142 | 143 | { 145 | this.onInputChange({id: 'birthday_day', value}); 146 | }} 147 | errorLabel={inputs.birthday_day.errorLabel} 148 | touched={inputs.birthday_day.touched} 149 | placeholder="Day" 150 | keyboardType="number-pad" 151 | /> 152 | 153 | 154 | { 156 | this.onInputChange({id: 'birthday_year', value}); 157 | }} 158 | errorLabel={inputs.birthday_year.errorLabel} 159 | touched={inputs.birthday_year.touched} 160 | placeholder="Year" 161 | keyboardType="number-pad" 162 | /> 163 | 164 | 165 | { 168 | this.onInputChange({id: 'state', value}); 169 | }} 170 | errorLabel={inputs.state.errorLabel} 171 | touched={inputs.state.touched} 172 | onLayout={({nativeEvent}) => { 173 | this.setInputPosition({ 174 | ids: ['state'], 175 | value: nativeEvent.layout.y, 176 | }); 177 | }} 178 | autoCapitalize="characters" 179 | maxLength={2} 180 | /> 181 | 182 | { 185 | this.onInputChange({id: 'zip', value}); 186 | }} 187 | errorLabel={inputs.zip.errorLabel} 188 | touched={inputs.zip.touched} 189 | onLayout={({nativeEvent}) => { 190 | this.setInputPosition({ 191 | ids: ['zip'], 192 | value: nativeEvent.layout.y, 193 | }); 194 | }} 195 | maxLength={5} 196 | keyboardType="number-pad" 197 | /> 198 | 199 | { 201 | this.setInputPosition({ 202 | ids: ['tos'], 203 | value: nativeEvent.layout.y, 204 | }); 205 | }}> 206 | 212 | { 215 | this.onInputChange({id: 'tos', value}); 216 | }} 217 | /> 218 | Do you agree to the TOS? 219 | 220 | {this.renderError('tos')} 221 | 222 | 223 | 224 | 225 |