diff --git a/README.md b/README.md index 4c29140..a04f28f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ +[![NPM Version](https://img.shields.io/npm/v/react-native-form-builder.svg?style=flat)](https://www.npmjs.com/package/react-native-form-builder) # react-native-form-builder ![alt text](http://g.recordit.co/7PqX8Ft7VO.gif) ![alt text](http://g.recordit.co/RWFvqi5tXG.gif) +# Note: +If you're looking for a better form management library with more advanced features, Please check out [React Reactive Form](https://github.com/bietkul/react-reactive-form). ## Features - Generate Form Fields UI - Manage, track state & values of fields - Automatically manages focus to next field on submit (TextInput) - Handle all keyboard related problems smartly - Supports custom validations & nested forms -- Uses Nativebase components +- Uses Nativebase components ## Getting Started @@ -30,10 +33,26 @@ + [Add Custom Validations](#add-custom-validations) + [Customize Your Form](#customize-your-form) + [Add custom components](#add-custom-components) + + [Add custome error component](#add-custom-error-component) - [Example](#example) ## Installation +`react-native-form-builder` requires a peer of [`native-base`](https://github.com/GeekyAnts/NativeBase) + + +To Install the peer dependecy +``` +$ npm i native-base --save + +``` +link the peer dependecy using + +``` +react-native link +``` +and then insteall `react-native-form-builder` + ```bash $ npm i react-native-form-builder --save ``` @@ -133,6 +152,8 @@ AppRegistry.registerComponent('FormGenerator', () => FormGenerator); | customComponents | N/A | `object` | To define your custom type of components.| | formData | N/A | `object` | To prefill the form values.| | fields | `required` | `array` | Array of form fields. | +| scrollViewProps | N/A | `object` | Scrollview custom props. | +| errorComponent | N/A | `React Component` | Custom error display component. | ### Methods: Currently, these methods are available in FormBuilder, you can access them by using ref property. @@ -264,7 +285,7 @@ If you're using array of objects then please don't forget to define these proper ```jsx objectType: true, labelKey: 'name', // For Below example -primaryKey: 'id, // For Below example +primaryKey: 'id', // For Below example ``` For e.g. ``` @@ -395,12 +416,13 @@ Build your custom type's components & handle them easily with the help of form b Use the `customComponents` prop of form builder. ### Prototype It's an object of key value pair where key will be the `type` of the component & value will be your custom Component.

-```customComponents = { type1: ComponentName1, type2: ComponentName2 .....}``` +```customComponents = { type1: {component: ComponentName1, props: Props }, type2: {component: ComponentName2} .....}``` ### How To Use - Define your custom `type` in field's object. - Form builder extends the props of your component by adding some extra props. -- In you component you can access these props to handle the state of the field. +- You can also pass some extra props in your custom components. +- In your component you can access these props to handle the state of the field. | Props | Type | Description | | :------------| :------| :-----| @@ -409,6 +431,10 @@ It's an object of key value pair where key will be the `type` of the component & | onSummitTextInput(fieldName) | `function`| If you're using TextInput then you can use this function to automatically manage the text input focus.For example you can define it in the `onSubmitEditing` prop of TextInput | theme | `object` | Use the theme variables to style your component +## Add Custom Error Component +- Now you can use your custom error component to display error messages. +- In your custom component you will receive two props `attributes` & `theme` variables. +- You can access the error & error message as a property of the attributes object. ## Example diff --git a/package.json b/package.json index e87fe1d..edfced1 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,26 @@ { - "name":"react-native-form-builder", - "version":"1.0.5", - "description":"Generate Awesome Forms In an easy way", - "main":"src/index.js", - "scripts":{ - "test":"echo \"Error: no test specified\" && exit 1" + "name": "react-native-form-builder", + "version": "1.0.16", + "description": "Generate Awesome Forms In an easy way", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" }, - "repository":{ - "type":"git", - "url":"git+https://github.com/bietkul/react-native-form-builder.git" + "repository": { + "type": "git", + "url": "git+https://github.com/bietkul/react-native-form-builder.git" }, - "_npmUser":{ - "name":"anjuma", - "email":"kuldepsaxena@155@gmail.com" + "_npmUser": { + "name": "anjuma", + "email": "kuldepsaxena@155@gmail.com" }, - "author":"Kuldeep Saxena", - "license":"MIT", - "bugs":{ - "url":"https://github.com/bietkul/react-native-form-builder/issues" + "author": "Kuldeep Saxena", + "license": "MIT", + "bugs": { + "url": "https://github.com/bietkul/react-native-form-builder/issues" }, - "homepage":"https://github.com/bietkul/react-native-form-builder#readme", - "keywords":[ + "homepage": "https://github.com/bietkul/react-native-form-builder#readme", + "keywords": [ "android", "ios", "react", @@ -32,16 +32,22 @@ "generator", "builder" ], - "dependencies":{ - "fs-extra":"^3.0.1", - "print-message":"^2.1.0", - "lodash":"^4.17.4", - "react-native-keyboard-aware-scroll-view":"^0.2.7", - "react-native-i18n":"^1.0.0" + "dependencies": { + "fs-extra": "^3.0.1", + "lodash": "^4.17.4", + "print-message": "^2.1.0", + "prop-types": "^15.6.0", + "react-native-i18n": "^2.0.0", + "react-native-keyboard-aware-scroll-view": "^0.8.0" }, - "devDependencies":{ - "react":"15.4.2", - "react-native":"0.41.1", - "native-base":"^2.0.6" + "devDependencies": { + "native-base": "^2.12.0", + "react": "^16.8.0", + "react-native": "0.60.0" + }, + "peerDependencies": { + "react-native": ">=0.60.0", + "react": ">=15.5.0", + "native-base": "^2.12.0" } } diff --git a/src/components/panel/index.js b/src/components/panel/index.js index 6cf7e8c..96938ea 100644 --- a/src/components/panel/index.js +++ b/src/components/panel/index.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Animated, Dimensions, View, Easing, Keyboard } from 'react-native'; import styles from './styles'; @@ -5,7 +6,7 @@ import styles from './styles'; class Panel extends Component { static propTypes = { - children: React.PropTypes.object, + children: PropTypes.object, } constructor(props) { @@ -55,4 +56,4 @@ class Panel extends Component { ); } } -export default Panel; +export default Panel; \ No newline at end of file diff --git a/src/fields/date/index.js b/src/fields/date/index.js index 777a1c5..b5d8e01 100644 --- a/src/fields/date/index.js +++ b/src/fields/date/index.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { View, Text } from 'native-base'; import I18n from 'react-native-i18n'; @@ -9,10 +10,11 @@ export default class DatePickerField extends Component { timeZoneOffsetInHours: (-1) * ((new Date()).getTimezoneOffset() / 60), }; static propTypes = { - attributes: React.PropTypes.object, - updateValue: React.PropTypes.func, - timeZoneOffsetInHours: React.PropTypes.number, - theme: React.PropTypes.object, + attributes: PropTypes.object, + updateValue: PropTypes.func, + timeZoneOffsetInHours: PropTypes.number, + theme: PropTypes.object, + ErrorComponent: PropTypes.func, } constructor(props) { super(props); @@ -69,7 +71,7 @@ export default class DatePickerField extends Component { } }; render() { - const { theme, attributes } = this.props; + const { theme, attributes, ErrorComponent } = this.props; const value = (attributes.value && new Date(attributes.value)) || null; const mode = attributes.mode || 'datetime'; return ( @@ -132,6 +134,7 @@ export default class DatePickerField extends Component { } + { this.panel = c; }} > @@ -196,11 +199,10 @@ export default class DatePickerField extends Component { } + - - } + } - ); } -} +} \ No newline at end of file diff --git a/src/fields/form/index.js b/src/fields/form/index.js index b920f63..7bc5532 100644 --- a/src/fields/form/index.js +++ b/src/fields/form/index.js @@ -1,14 +1,16 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { View, Text } from 'native-base'; import GenerateForm from '../../formBuilder'; export default class FormField extends Component { static propTypes = { - attributes: React.PropTypes.object, - theme: React.PropTypes.object, - updateValue: React.PropTypes.func, - autoValidation: React.PropTypes.bool, - customValidation: React.PropTypes.func, + attributes: PropTypes.object, + theme: PropTypes.object, + updateValue: PropTypes.func, + autoValidation: PropTypes.bool, + customValidation: PropTypes.func, + customComponents: PropTypes.object, } constructor(props) { super(props); @@ -31,6 +33,7 @@ export default class FormField extends Component { theme, autoValidation, customValidation, + customComponents, } = this.props; return ( @@ -43,6 +46,7 @@ export default class FormField extends Component { onValueChange={this.onValueChange} autoValidation={autoValidation} customValidation={customValidation} + customComponents={customComponents} showErrors fields={attributes.fields} theme={theme} @@ -51,4 +55,4 @@ export default class FormField extends Component { ); } -} +} \ No newline at end of file diff --git a/src/fields/picker/index.js b/src/fields/picker/index.js index 6bba4bb..655d700 100644 --- a/src/fields/picker/index.js +++ b/src/fields/picker/index.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { View, Text } from 'native-base'; import { Platform, Picker, TouchableOpacity } from 'react-native'; @@ -7,26 +8,27 @@ import styles from './../../styles'; const Item = Picker.Item; export default class PickerField extends Component { static propTypes = { - attributes: React.PropTypes.object, - theme: React.PropTypes.object, - updateValue: React.PropTypes.func, + attributes: PropTypes.object, + theme: PropTypes.object, + updateValue: PropTypes.func, + ErrorComponent: PropTypes.func, } handleChange(value) { const attributes = this.props.attributes; this.props.updateValue(attributes.name, attributes.options[value]); } render() { - const { theme, attributes } = this.props; + const { theme, attributes, ErrorComponent } = this.props; const isValueValid = attributes.options.indexOf(attributes.value) > -1; const pickerValue = attributes.options.indexOf(attributes.value).toString(); if (Platform.OS !== 'ios') { return ( {attributes.label} @@ -42,12 +44,12 @@ export default class PickerField extends Component { >{ attributes.options.map((item, index) => ( - )) + )) } + - ); } return ( @@ -75,6 +77,7 @@ export default class PickerField extends Component { {isValueValid ? attributes.value : 'None'} + { this.panel = c; }} @@ -97,4 +100,4 @@ export default class PickerField extends Component { ); } -} +} \ No newline at end of file diff --git a/src/fields/select/index.js b/src/fields/select/index.js index ec3f85c..bfa59cb 100644 --- a/src/fields/select/index.js +++ b/src/fields/select/index.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { Modal, Dimensions } from 'react-native'; import { @@ -20,9 +21,10 @@ const deviceWidth = Dimensions.get('window').width; export default class SelectField extends Component { static propTypes = { - attributes: React.PropTypes.object, - updateValue: React.PropTypes.func, - theme: React.PropTypes.object, + attributes: PropTypes.object, + updateValue: PropTypes.func, + theme: PropTypes.object, + ErrorComponent: PropTypes.func, } constructor(props) { super(props); @@ -53,7 +55,7 @@ export default class SelectField extends Component { }, () => this.props.updateValue(this.props.attributes.name, newSelected)); } render() { - const { attributes } = this.props; + const { theme, attributes, ErrorComponent } = this.props; const selectedText = attributes.multiple ? attributes.value.length || 'None' : attributes.objectType ? @@ -73,6 +75,9 @@ export default class SelectField extends Component { + + + ); } -} +} \ No newline at end of file diff --git a/src/fields/switch/index.js b/src/fields/switch/index.js index fb436f7..9a172b7 100644 --- a/src/fields/switch/index.js +++ b/src/fields/switch/index.js @@ -1,40 +1,46 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { View, Text, Switch } from 'native-base'; export default class SwitchField extends Component { static propTypes = { - attributes: React.PropTypes.object, - theme: React.PropTypes.object, - updateValue: React.PropTypes.func, + attributes: PropTypes.object, + theme: PropTypes.object, + updateValue: PropTypes.func, + ErrorComponent: PropTypes.func, } handleChange(value) { this.props.updateValue(this.props.attributes.name, value); } render() { - const attributes = this.props.attributes; - const theme = this.props.theme; + const { attributes, theme, ErrorComponent } = this.props; return ( - - {attributes.label} - this.handleChange(value)} - value={attributes.value} - /> + + + {attributes.label} + this.handleChange(value)} + value={attributes.value} + /> + + + + ); } -} +} \ No newline at end of file diff --git a/src/fields/textInput/index.js b/src/fields/textInput/index.js index 0fb1aef..9a2715a 100644 --- a/src/fields/textInput/index.js +++ b/src/fields/textInput/index.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { View, Item, Input, Icon, ListItem, Text } from 'native-base'; import { Platform } from 'react-native'; @@ -5,25 +6,26 @@ import { getKeyboardType } from '../../utils/methods'; export default class TextInputField extends Component { static propTypes = { - attributes: React.PropTypes.object, - theme: React.PropTypes.object, - updateValue: React.PropTypes.func, - onSummitTextInput: React.PropTypes.func, + attributes: PropTypes.object, + theme: PropTypes.object, + updateValue: PropTypes.func, + onSummitTextInput: PropTypes.func, + ErrorComponent: PropTypes.func, } handleChange(text) { this.props.updateValue(this.props.attributes.name, text); } render() { - const { theme, attributes } = this.props; + const { theme, attributes, ErrorComponent } = this.props; const inputProps = attributes.props; const keyboardType = getKeyboardType(attributes.type); return ( - + { attributes.icon && - + } this.handleChange(text)} {...inputProps} /> - { attributes.error ? - : null} + { theme.textInputErrorIcon && attributes.error ? + : null} - - {attributes.errorMsg} - + - ); } } diff --git a/src/formBuilder/index.js b/src/formBuilder/index.js index 1c8987f..30f41d9 100644 --- a/src/formBuilder/index.js +++ b/src/formBuilder/index.js @@ -1,6 +1,8 @@ +import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { View, Keyboard } from 'react-native'; +import { View, Keyboard, Text } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import _ from 'lodash'; import TextInputField from '../fields/textInput'; import PickerField from '../fields/picker'; import SwitchField from '../fields/switch'; @@ -10,22 +12,37 @@ import FormField from '../fields/form'; import baseTheme from '../theme'; import { autoValidate, getInitState, getDefaultValue, getResetValue } from '../utils/methods'; - +const DefaultErrorComponent = (props) => { + const attributes = props.attributes; + const theme = props.theme; + if (attributes.error) { + return ( + + { attributes.errorMsg } + + ); + } + return null; +}; export default class FormBuilder extends Component { static propTypes = { - fields: React.PropTypes.array, - theme: React.PropTypes.object, - customComponents: React.PropTypes.object, - formData: React.PropTypes.object, - autoValidation: React.PropTypes.bool, - customValidation: React.PropTypes.func, - onValueChange: React.PropTypes.func, + fields: PropTypes.array, + theme: PropTypes.object, + scrollViewProps: PropTypes.object, + customComponents: PropTypes.object, + formData: PropTypes.object, + errorComponent: PropTypes.func, + autoValidation: PropTypes.bool, + customValidation: PropTypes.func, + onValueChange: PropTypes.func, } constructor(props) { super(props); const initialState = getInitState(props.fields); this.state = { - ...initialState, + fields: { + ...initialState, + }, errorStatus: false, }; // Supports Nested @@ -38,6 +55,8 @@ export default class FormBuilder extends Component { this.setValues = this.setValues.bind(this); // forcefully set default values for particular fields this.setToDefault = this.setToDefault.bind(this); + // update the form fields + this.updateState = this.updateState.bind(this); /* forcefully set errors for a particular field this.setErrors = this.setErrors.bind(this); @@ -56,17 +75,43 @@ export default class FormBuilder extends Component { const { formData } = this.props; this.setValues(formData); } + componentDidUpdate(prevProps) { + if (!_.isEqual(prevProps.fields, this.props.fields)) { + const nextState = this.updateState(this.props.fields); + + let fields = Object.assign({}, this.state.fields, nextState.fields); + fields = _.omit(fields, nextState.hiddenFields); + + this.setState({ fields }); + } + } + updateState(fields) { + const newFields = {}; + const hiddenFields = []; + _.forEach(fields, (field) => { + const fieldObj = field; + if (!field.hidden && field.type) { + const stateField = this.state.fields[field.name]; + fieldObj.value = stateField && stateField.value ? stateField.value : getDefaultValue(field); + newFields[field.name] = fieldObj; + } else if (field.hidden) { + hiddenFields.push(field.name); + } + }); + return { fields: newFields, hiddenFields }; + } onSummitTextInput(name) { - const index = Object.keys(this.state).indexOf(name); - if (index !== -1 && this[Object.keys(this.state)[index + 1]] - && this[Object.keys(this.state)[index + 1]].textInput) { - this[Object.keys(this.state)[index + 1]].textInput._root.focus(); + const { fields } = this.state; + const index = Object.keys(fields).indexOf(name); + if (index !== -1 && this[Object.keys(fields)[index + 1]] + && this[Object.keys(fields)[index + 1]].textInput) { + this[Object.keys(fields)[index + 1]].textInput._root.focus(); } else { Keyboard.dismiss(); } } onValueChange(name, value) { - const valueObj = this.state[name]; + const valueObj = this.state.fields[name]; if (valueObj) { valueObj.value = value; // Not Validate fields only when autoValidation prop is false @@ -75,7 +120,7 @@ export default class FormBuilder extends Component { } // Validate through customValidation if it is present in props if (this.props.customValidation - && typeof this.props.customValidation === 'function') { + && typeof this.props.customValidation === 'function') { Object.assign(valueObj, this.props.customValidation(valueObj)); } const newField = {}; @@ -83,17 +128,27 @@ export default class FormBuilder extends Component { // this.props.customValidation(valueObj); if (this.props.onValueChange && typeof this.props.onValueChange === 'function') { - this.setState({ ...newField }, () => this.props.onValueChange()); + this.setState({ + fields: { + ...this.state.fields, + ...newField, + } + }, () => this.props.onValueChange()); } else { - this.setState({ ...newField }); + this.setState({ + fields: { + ...this.state.fields, + ...newField, + } + }); } } } // Returns the values of the fields getValues() { const values = {}; - Object.keys(this.state).forEach((fieldName) => { - const field = this.state[fieldName]; + Object.keys(this.state.fields).forEach((fieldName) => { + const field = this.state.fields[fieldName]; if (field) { values[field.name] = field.value; } @@ -105,7 +160,7 @@ export default class FormBuilder extends Component { const field = fieldObj; if (field.type === 'group') { const allFields = []; - this.state[field.name].fields.forEach((item) => { + this.state.fields[field.name].fields.forEach((item) => { allFields.push(item.name); }); this[field.name].group.setToDefault(allFields); @@ -127,19 +182,29 @@ export default class FormBuilder extends Component { if (typeof args[0] === 'object') { const newFields = {}; args[0].forEach((item) => { - const field = this.state[item]; + const field = this.state.fields[item]; if (field) { field.value = getDefaultValue(field); newFields[field.name] = this.getFieldDefaultValue(field); } }); - this.setState({ ...newFields }); + this.setState({ + fields: { + ...this.state.fields, + ...newFields, + } + }); } else { - const field = this.state[args[0]]; + const field = this.state.fields[args[0]]; if (field) { const newField = {}; newField[field.name] = this.getFieldDefaultValue(field); - this.setState({ ...newField }); + this.setState({ + fields: { + ...this.state.fields, + ...newFields, + } + }); } } } @@ -157,13 +222,13 @@ export default class FormBuilder extends Component { // Remaing thing is error Handling Here } else { field.value = value; - // also check for errors + // also check for errors if (this.props.autoValidation === undefined || this.props.autoValidation) { Object.assign(field, autoValidate(field)); } - // Validate through customValidation if it is present in props + // Validate through customValidation if it is present in props if (this.props.customValidation - && typeof this.props.customValidation === 'function') { + && typeof this.props.customValidation === 'function') { Object.assign(field, this.props.customValidation(field)); } } @@ -176,19 +241,24 @@ export default class FormBuilder extends Component { if (args && args.length && args[0]) { const newFields = {}; Object.keys(args[0]).forEach((fieldName) => { - const field = this.state[fieldName]; + const field = this.state.fields[fieldName]; if (field) { newFields[field.name] = this.getFieldValue(field, args[0][fieldName]); } }); - this.setState({ ...newFields }); + this.setState({ + fields: { + ...this.state.fields, + ...newFields, + } + }); } } // Reset Form values & errors NESTED SUPPORTED resetForm() { const newFields = {}; - Object.keys(this.state).forEach((fieldName) => { - const field = this.state[fieldName]; + Object.keys(this.state.fields).forEach((fieldName) => { + const field = this.state.fields[fieldName]; if (field) { field.value = (field.editable !== undefined && !field.editable) ? getDefaultValue(field) : @@ -201,27 +271,37 @@ export default class FormBuilder extends Component { newFields[field.name] = field; } }); - this.setState({ ...newFields }); + this.setState({ + fields: { + ...this.state.fields, + ...newFields, + } + }); } generateFields() { const theme = Object.assign(baseTheme, this.props.theme); - const { customComponents } = this.props; - const renderFields = Object.keys(this.state).map((fieldName, index) => { - const field = this.state[fieldName]; - if (!field.hidden) { + const { customComponents, errorComponent, fields } = this.props; + // Use fields from props to maintain the order of the props if the hidden prop is changed + const renderFields = fields.map(({ name: fieldName }, index) => { + const field = this.state.fields[fieldName]; + if (field && !field.hidden) { const commonProps = { key: index, theme, - attributes: this.state[field.name], + attributes: this.state.fields[field.name], updateValue: this.onValueChange, + ErrorComponent: errorComponent || DefaultErrorComponent, }; if (customComponents) { - const CustomComponent = customComponents[field.type]; - if (CustomComponent) { + const CustomComponentObj = customComponents[field.type]; + if (CustomComponentObj) { + const CustomComponent = CustomComponentObj.component; + const CustomComponentProps = CustomComponentObj.props; return ( { this[field.name] = c; }} {... commonProps} + {...CustomComponentProps} onSummitTextInput={this.onSummitTextInput} /> ); @@ -288,6 +368,7 @@ export default class FormBuilder extends Component { {this.generateFields() || } @@ -296,4 +377,4 @@ export default class FormBuilder extends Component { ); } -} +} \ No newline at end of file diff --git a/src/theme.js b/src/theme.js index f241030..0e7ba01 100644 --- a/src/theme.js +++ b/src/theme.js @@ -1,5 +1,3 @@ -import Color from 'color'; - import { Platform } from 'react-native'; export default { @@ -14,4 +12,7 @@ export default { inputColor: '#575757', inputFontSize: 15, labelActiveColor: '#575757', + errorMsgColor: '#ed2f2f', + changeTextInputColorOnError: true, + textInputErrorIcon: 'close-circle', }; diff --git a/src/utils/methods.js b/src/utils/methods.js index 1dd5ef4..11f35b0 100644 --- a/src/utils/methods.js +++ b/src/utils/methods.js @@ -16,16 +16,16 @@ export function autoValidate(field) { let errorMsg = ''; if (field.required) { switch (field.type) { - case 'text': case 'email': if (isEmpty(field.value)) { error = true; errorMsg = `${field.label} is required`; } else if (!isEmail(field.value)) { error = true; - errorMsg = 'Please Enter a valid Email'; + errorMsg = 'Please enter a valid email'; } break; + case 'text': case 'url': case 'password': if (isEmpty(field.value)) { @@ -39,7 +39,7 @@ export function autoValidate(field) { error = true; errorMsg = `${field.label} is required`; } else if (isNaN(field.value)) { - errorMsg = `${field.label} Should be a number`; + errorMsg = `${field.label} should be a number`; } } break;