├── lib ├── components │ ├── Select │ │ ├── styles.js │ │ └── index.js │ ├── RadioGroup │ │ ├── styles.js │ │ └── index.js │ ├── CustomDate │ │ ├── styles.js │ │ └── index.js │ ├── Rating │ │ ├── styles.js │ │ └── index.js │ ├── Button │ │ ├── styles.js │ │ └── index.js │ ├── CheckboxGroup │ │ ├── styles.js │ │ └── index.js │ ├── Paragraph │ │ └── index.js │ ├── NumberSelector │ │ ├── styles.js │ │ └── index.js │ ├── Header │ │ └── index.js │ ├── CustomInput │ │ ├── styles.js │ │ └── index.js │ └── LabelError │ │ └── index.js ├── DynamicForm │ ├── styles.js │ └── index.js └── util.js ├── config ├── scale.js ├── colors.js └── styles.js ├── index.js ├── .eslintrc ├── LICENSE ├── .gitignore ├── package.json └── README.md /lib/components/Select/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({}); 4 | -------------------------------------------------------------------------------- /lib/components/RadioGroup/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | radioContainer: {}, 3 | otherRow: { 4 | flexDirection: 'row', 5 | alignItems: 'center', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/components/CustomDate/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | dateContainer: { 5 | flex: 1, 6 | marginTop: 10, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /config/scale.js: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native'; 2 | 3 | const { 4 | height: deviceHeight, 5 | width: deviceWidth, 6 | } = Dimensions.get('window'); 7 | 8 | export { 9 | deviceHeight, 10 | deviceWidth, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/components/Rating/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ratingContainer: { 3 | marginTop: 5, 4 | flexDirection: 'row', 5 | alignItems: 'center', 6 | }, 7 | containerStyle: { 8 | marginRight: 10, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /lib/DynamicForm/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | padding: 16, 7 | }, 8 | row: { 9 | marginBottom: 10, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | export const getInputType = (subtype) => { 2 | switch (subtype) { 3 | case 'text': 4 | return 'default'; 5 | case 'tel': 6 | return 'phone-pad'; 7 | case 'email': 8 | return 'email-address'; 9 | default: 10 | return 'default'; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // main package 2 | import DynamicForm from './lib/DynamicForm'; 3 | 4 | // theming config 5 | import { 6 | defaultColors as colors, 7 | defaultFonts as fonts, 8 | defaultTheme as theme, 9 | buildTheme as builder, 10 | } from './config/styles'; 11 | 12 | export const defaultColors = colors; 13 | export const defaultFonts = fonts; 14 | export const defaultTheme = theme; 15 | export const buildTheme = builder; 16 | 17 | export default DynamicForm; 18 | -------------------------------------------------------------------------------- /lib/components/Button/styles.js: -------------------------------------------------------------------------------- 1 | import { primary, white } from '../../../config/colors'; 2 | 3 | export default { 4 | buttonContainer: { 5 | margin: 20, 6 | flexDirection: 'row', 7 | alignItems: 'center', 8 | height: 50, 9 | elevation: 0, 10 | backgroundColor: primary, 11 | borderRadius: 3, 12 | }, 13 | buttonLabel: { 14 | flex: 1, 15 | color: white, 16 | fontSize: 14, 17 | textAlign: 'center', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/components/CheckboxGroup/styles.js: -------------------------------------------------------------------------------- 1 | import { 2 | textPrimary, 3 | } from '../../../config/colors'; 4 | 5 | export default { 6 | checkboxContainer: {}, 7 | otherRow: { 8 | flexDirection: 'row', 9 | alignItems: 'center', 10 | }, 11 | switchRow: { 12 | flexDirection: 'row', 13 | alignItems: 'center', 14 | marginBottom: 5, 15 | }, 16 | toggleText: { 17 | fontSize: 16, 18 | color: textPrimary, 19 | marginLeft: 20, 20 | marginRight: 20, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/components/Paragraph/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Paragraph = ({ label, style }, { theme }) => { 6 | const paragraphStyle = theme.p; 7 | return ( 8 | 9 | {label} 10 | 11 | ); 12 | }; 13 | 14 | Paragraph.propTypes = { 15 | label: PropTypes.string.isRequired, 16 | style: PropTypes.object, 17 | }; 18 | 19 | Paragraph.defaultProps = { 20 | style: {}, 21 | }; 22 | 23 | Paragraph.contextTypes = { 24 | theme: PropTypes.object.isRequired, 25 | }; 26 | 27 | export default Paragraph; 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": "airbnb", 8 | "rules": { 9 | "react/jsx-filename-extension": [ 10 | "error", 11 | { 12 | "extensions": [ 13 | ".js", 14 | ".jsx" 15 | ] 16 | } 17 | ], 18 | "no-trailing-spaces": "off", 19 | "react/forbid-prop-types": 0, 20 | "no-underscore-dangle": "off", 21 | "prefer-template": "off", 22 | "no-restricted-globals": "off", 23 | "no-console": 0, 24 | "no-param-reassign": 0, 25 | "no-return-assign": 0, 26 | "no-case-declarations": 0 27 | } 28 | } -------------------------------------------------------------------------------- /lib/components/NumberSelector/styles.js: -------------------------------------------------------------------------------- 1 | import { 2 | textPrimary, 3 | placeholderTextColor, 4 | } from '../../../config/colors'; 5 | 6 | export default { 7 | inputContainer: { 8 | marginTop: 5, 9 | flexDirection: 'row', 10 | alignItems: 'center', 11 | justifyContent: 'center', 12 | borderColor: placeholderTextColor, 13 | borderWidth: 1, 14 | borderRadius: 3, 15 | }, 16 | input: { 17 | flex: 1, 18 | fontSize: 20, 19 | textAlign: 'left', 20 | color: textPrimary, 21 | paddingLeft: 10, 22 | marginRight: 10, 23 | opacity: 0.8, 24 | }, 25 | controllersContainer: { 26 | padding: 5, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /config/colors.js: -------------------------------------------------------------------------------- 1 | export const primary = '#00a5ff'; 2 | export const primaryDark = '#0077cb'; 3 | export const textPrimary = '#2A3C53'; 4 | export const error = '#FF6565'; 5 | export const success = '#50e3c2'; 6 | export const iconDark = 'rgba(0,0,0,0.4)'; 7 | export const textInputBorderColor = '#cac8c8'; 8 | export const placeholderTextColor = '#A9A9A9'; 9 | export const starFillColor = '#f5a623'; 10 | export const dividerLine = '#dedede'; 11 | 12 | export const grey500 = '#9e9e9e'; 13 | export const grey800 = '#424242'; 14 | export const grey400 = '#bdbdbd'; 15 | export const red700 = '#d32f2f'; 16 | 17 | export const black = '#000000'; 18 | export const white = '#ffffff'; 19 | 20 | export default {}; 21 | -------------------------------------------------------------------------------- /lib/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import _ from 'lodash'; 5 | 6 | const Header = ({ label, subType, style }, { theme }) => { 7 | const headerTheme = theme.headers; 8 | const headerStyle = _.get(headerTheme, subType); 9 | return ( 10 | 11 | {label} 12 | 13 | ); 14 | }; 15 | 16 | Header.propTypes = { 17 | label: PropTypes.string.isRequired, 18 | subType: PropTypes.string.isRequired, 19 | style: PropTypes.object, 20 | }; 21 | 22 | Header.defaultProps = { 23 | style: {}, 24 | }; 25 | 26 | Header.contextTypes = { 27 | theme: PropTypes.object.isRequired, 28 | }; 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /lib/components/CustomInput/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform } from 'react-native'; 2 | 3 | import { 4 | error as errorColor, 5 | textPrimary, 6 | textInputBorderColor, 7 | } from '../../../config/colors'; 8 | 9 | export default ( 10 | fontSize, 11 | error, 12 | icon, 13 | disabled, 14 | showBorder, 15 | multiline, 16 | ) => ( 17 | StyleSheet.create({ 18 | container: { 19 | borderBottomWidth: showBorder ? 1 : 0, 20 | borderBottomColor: textInputBorderColor, 21 | opacity: disabled ? 0.7 : 1, 22 | }, 23 | inputStyle: { 24 | height: multiline ? 100 : 50, 25 | borderWidth: 0, 26 | textAlignVertical: 'center', 27 | fontSize, 28 | color: error ? errorColor : textPrimary, 29 | paddingRight: icon ? 25 : 0, 30 | marginTop: (Platform.OS === 'ios' && multiline) ? 15 : 5, 31 | }, 32 | iconStyle: { 33 | position: 'absolute', 34 | right: 0, 35 | top: 0, 36 | bottom: 0, 37 | justifyContent: 'center', 38 | }, 39 | }) 40 | ); 41 | -------------------------------------------------------------------------------- /lib/components/LabelError/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | } from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | 8 | 9 | export default class LabelError extends PureComponent { 10 | static propTypes = { 11 | label: PropTypes.string, 12 | error: PropTypes.bool, 13 | }; 14 | 15 | static defaultProps = { 16 | label: '', 17 | error: false, 18 | }; 19 | 20 | static contextTypes = { 21 | theme: PropTypes.object.isRequired, 22 | }; 23 | 24 | render() { 25 | const { label, error } = this.props; 26 | const { theme } = this.context; 27 | return ( 28 | 29 | { 30 | label 31 | ? 32 | 33 | {label} 34 | 35 | : 36 | null 37 | } 38 | { 39 | error 40 | ? 41 | 42 | Required 43 | 44 | : 45 | null 46 | } 47 | 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mustapha Babatunde 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /lib/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | Text, 5 | } from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | 8 | import styles from './styles'; 9 | 10 | const Button = ({ 11 | label, 12 | onPress, 13 | buttonStyle, 14 | buttonTextStyle, 15 | disabled, 16 | }) => ( 17 | 28 | 34 | {label} 35 | 36 | 37 | ); 38 | 39 | Button.propTypes = { 40 | label: PropTypes.string.isRequired, 41 | onPress: PropTypes.func, 42 | buttonStyle: PropTypes.oneOfType([ 43 | PropTypes.object, 44 | PropTypes.number, 45 | ]), 46 | buttonTextStyle: PropTypes.oneOfType([ 47 | PropTypes.object, 48 | PropTypes.number, 49 | ]), 50 | disabled: PropTypes.bool, 51 | }; 52 | 53 | Button.defaultProps = { 54 | onPress: () => {}, 55 | buttonStyle: {}, 56 | buttonTextStyle: {}, 57 | disabled: false, 58 | }; 59 | 60 | export default Button; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-dynamic-form", 3 | "version": "0.0.1", 4 | "description": "Dynamic form builder for React Native", 5 | "main": "index.js", 6 | "repository": "git@github.com:toystars/react-native-dynamic-form.git", 7 | "author": "Mustapha Babatunde ", 8 | "license": "MIT", 9 | "private": false, 10 | "scripts": { 11 | "lint": "eslint lib/" 12 | }, 13 | "peerDependencies": { 14 | "react": "^16.3.2", 15 | "react-native": "^0.55.4", 16 | "react-native-vector-icons": "^4.6.0" 17 | }, 18 | "dependencies": { 19 | "lodash": "^4.17.10", 20 | "moment": "^2.22.1", 21 | "prop-types": "^15.6.1", 22 | "react-native-datepicker": "^1.7.2", 23 | "react-native-material-ui": "^1.22.3", 24 | "react-native-multiple-select": "^0.4.4", 25 | "react-native-star-rating": "^1.0.9" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^8.2.3", 29 | "babel-jest": "22.4.3", 30 | "babel-plugin-module-resolver": "^3.1.1", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-preset-react-native": "4.0.0", 33 | "eslint": "^4.19.1", 34 | "eslint-config-airbnb": "^16.1.0", 35 | "eslint-nibble": "^4.2.1", 36 | "eslint-plugin-import": "^2.11.0", 37 | "eslint-plugin-jsx-a11y": "^6.0.3", 38 | "eslint-plugin-react": "^7.7.0", 39 | "jest": "22.4.3", 40 | "jest-react-native": "^18.0.0", 41 | "pre-commit": "^1.2.2", 42 | "react-test-renderer": "16.3.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/components/CustomDate/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | } from 'react-native'; 5 | import PropTypes from 'prop-types'; 6 | import DatePicker from 'react-native-datepicker'; 7 | 8 | import LabelError from '../LabelError'; 9 | 10 | import styles from './styles'; 11 | 12 | export default class CustomDate extends Component { 13 | static propTypes = { 14 | label: PropTypes.string, 15 | value: PropTypes.any, 16 | placeholder: PropTypes.string, 17 | onDateChange: PropTypes.func, 18 | disabled: PropTypes.bool, 19 | minDate: PropTypes.any, 20 | maxDate: PropTypes.any, 21 | dateFormat: PropTypes.string, 22 | error: PropTypes.bool, 23 | }; 24 | 25 | static defaultProps = { 26 | label: '', 27 | value: '', 28 | placeholder: '', 29 | onDateChange: () => {}, 30 | disabled: false, 31 | minDate: null, 32 | maxDate: null, 33 | dateFormat: 'DD-MM-YYYY', 34 | error: false, 35 | }; 36 | 37 | onDateChange = (date) => { 38 | const { onDateChange } = this.props; 39 | onDateChange(date); 40 | } 41 | 42 | render() { 43 | const { 44 | label, 45 | value, 46 | placeholder, 47 | disabled, 48 | minDate, 49 | maxDate, 50 | dateFormat, 51 | error, 52 | } = this.props; 53 | const moreOptions = {}; 54 | if (minDate) { 55 | moreOptions.minDate = minDate; 56 | } 57 | if (maxDate) { 58 | moreOptions.maxDate = maxDate; 59 | } 60 | return ( 61 | 62 | 66 | 89 | 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/components/Select/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { View } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import MultiSelect from 'react-native-multiple-select'; 5 | 6 | import LabelError from '../LabelError'; 7 | 8 | export default class Select extends PureComponent { 9 | static propTypes = { 10 | data: PropTypes.array.isRequired, 11 | label: PropTypes.string, 12 | values: PropTypes.array, 13 | onSelect: PropTypes.func, 14 | single: PropTypes.bool, 15 | searchInputPlaceholder: PropTypes.string, 16 | error: PropTypes.bool, 17 | }; 18 | 19 | static defaultProps = { 20 | label: '', 21 | values: [], 22 | single: true, 23 | searchInputPlaceholder: 'Search Items...', 24 | onSelect: () => {}, 25 | error: false, 26 | }; 27 | 28 | static contextTypes = { 29 | theme: PropTypes.object.isRequired, 30 | }; 31 | 32 | onSelectedItemsChange = (selectedItems) => { 33 | const { onSelect } = this.props; 34 | onSelect(selectedItems); 35 | } 36 | 37 | render() { 38 | const { 39 | label, 40 | values, 41 | data, 42 | single, 43 | searchInputPlaceholder, 44 | error, 45 | } = this.props; 46 | const { 47 | theme: { 48 | select: { 49 | tagRemoveIconColor, 50 | tagBorderColor, 51 | tagTextColor, 52 | selectedItemTextColor, 53 | selectedItemIconColor, 54 | itemTextColor, 55 | submitButtonColor, 56 | }, 57 | }, 58 | } = this.context; 59 | return ( 60 | 61 | 65 | 66 | 87 | 88 | 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/components/Rating/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | } from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | import StarRating from 'react-native-star-rating'; 8 | 9 | import LabelError from '../LabelError'; 10 | 11 | import styles from './styles'; 12 | 13 | export default class Rating extends PureComponent { 14 | static propTypes = { 15 | label: PropTypes.string, 16 | onStarRatingChange: PropTypes.func, 17 | starCount: PropTypes.number, 18 | config: PropTypes.object, 19 | maxStars: PropTypes.number, 20 | error: PropTypes.bool, 21 | }; 22 | 23 | static defaultProps = { 24 | label: '', 25 | onStarRatingChange: () => {}, 26 | starCount: 0, 27 | config: { 28 | iconSet: 'MaterialIcons', 29 | emptyStar: 'star-border', 30 | fullStar: 'star', 31 | halfStar: 'star-half', 32 | enableHalfStar: false, 33 | ratingRemark: this.ratingRemark, 34 | }, 35 | maxStars: 5, 36 | error: false, 37 | }; 38 | 39 | static contextTypes = { 40 | theme: PropTypes.object.isRequired, 41 | }; 42 | 43 | constructor(props) { 44 | super(props); 45 | this.ratingRemark = { 46 | 1: 'Very Bad', 47 | 2: 'Bad', 48 | 3: 'Average', 49 | 4: 'Good', 50 | 5: 'Excellent', 51 | }; 52 | } 53 | 54 | onStarRatingPress = (starCount) => { 55 | const { onStarRatingChange } = this.props; 56 | onStarRatingChange(starCount); 57 | }; 58 | 59 | getRatingRemark = () => { 60 | const { starCount, config } = this.props; 61 | const ratingRemark = config.ratingRemark || this.ratingRemark; 62 | return ratingRemark[starCount] || ''; 63 | }; 64 | 65 | render() { 66 | const { 67 | label, 68 | starCount, 69 | error, 70 | maxStars, 71 | config, 72 | } = this.props; 73 | const { 74 | iconSet, 75 | emptyStar, 76 | fullStar, 77 | halfStar, 78 | enableHalfStar, 79 | } = config; 80 | const { theme } = this.context; 81 | return ( 82 | 83 | 87 | 88 | 101 | 102 | {this.getRatingRemark()} 103 | 104 | 105 | 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/components/RadioGroup/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | } from 'react-native'; 5 | import PropTypes from 'prop-types'; 6 | import { RadioButton } from 'react-native-material-ui'; 7 | import _ from 'lodash'; 8 | 9 | import LabelError from '../LabelError'; 10 | import CustomInput from '../CustomInput'; 11 | 12 | import styles from './styles'; 13 | 14 | export default class RadioGroup extends Component { 15 | static propTypes = { 16 | label: PropTypes.string, 17 | options: PropTypes.array.isRequired, 18 | onRadioValueChanged: PropTypes.func, 19 | other: PropTypes.bool, 20 | value: PropTypes.string, 21 | error: PropTypes.bool, 22 | }; 23 | 24 | static defaultProps = { 25 | label: '', 26 | onRadioValueChanged: () => {}, 27 | other: false, 28 | value: '', 29 | error: false, 30 | }; 31 | 32 | state = { 33 | selectedValue: '', 34 | textValue: '', 35 | }; 36 | 37 | onCheck = (value) => { 38 | const { onRadioValueChanged } = this.props; 39 | const { selectedValue } = this.state; 40 | const newValue = value.value; 41 | if (newValue !== 'other') { 42 | this.setState({ 43 | selectedValue: newValue, 44 | textValue: '', 45 | }); 46 | onRadioValueChanged(newValue); 47 | } else { 48 | if (selectedValue !== 'other') { 49 | onRadioValueChanged(''); 50 | } 51 | this.setState({ 52 | selectedValue: newValue, 53 | }); 54 | } 55 | } 56 | 57 | onOtherTextChanged = (text) => { 58 | const { onRadioValueChanged } = this.props; 59 | onRadioValueChanged(text); 60 | }; 61 | 62 | renderOtherInput = () => { 63 | const { 64 | selectedValue, 65 | textValue, 66 | } = this.state; 67 | if (selectedValue === 'other') { 68 | return ( 69 | v} 73 | onChangeText={this.onOtherTextChanged} 74 | /> 75 | ); 76 | } 77 | return null; 78 | }; 79 | 80 | render() { 81 | const { 82 | label, 83 | options, 84 | error, 85 | other, 86 | } = this.props; 87 | const { selectedValue } = this.state; 88 | const propValue = this.props.value; 89 | return ( 90 | 91 | 95 | 96 | { 97 | _.map(options, value => ( 98 | {}} 104 | onCheck={(checked) => { 105 | this.onCheck(value, checked); 106 | }} 107 | /> 108 | )) 109 | } 110 | { 111 | other 112 | ? 113 | 114 | {}} 120 | onCheck={(checked) => { 121 | this.onCheck({ 122 | value: 'other', 123 | label: 'Other', 124 | }, checked); 125 | }} 126 | /> 127 | 128 | {this.renderOtherInput()} 129 | 130 | 131 | : 132 | null 133 | } 134 | 135 | 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/components/NumberSelector/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | View, 4 | TextInput, 5 | Text, 6 | TouchableOpacity, 7 | } from 'react-native'; 8 | import PropTypes from 'prop-types'; 9 | import Icon from 'react-native-vector-icons/Ionicons'; 10 | 11 | import LabelError from '../LabelError'; 12 | 13 | import { 14 | textPrimary, 15 | iconDark, 16 | } from '../../../config/colors'; 17 | import styles from './styles'; 18 | 19 | export default class NumberSelector extends PureComponent { 20 | static propTypes = { 21 | label: PropTypes.string, 22 | max: PropTypes.number, 23 | min: PropTypes.number.isRequired, 24 | step: PropTypes.number, 25 | value: PropTypes.number, 26 | placeholder: PropTypes.any, 27 | onNumberChanged: PropTypes.func, 28 | disabled: PropTypes.bool, 29 | directTextEdit: PropTypes.bool, 30 | error: PropTypes.bool, 31 | }; 32 | 33 | static defaultProps = { 34 | label: '', 35 | max: null, 36 | step: 1, 37 | value: 0, 38 | onNumberChanged: () => {}, 39 | placeholder: 0, 40 | disabled: false, 41 | directTextEdit: false, 42 | error: false, 43 | }; 44 | 45 | constructor(props) { 46 | super(props); 47 | this.state = { 48 | value: this.props.value, 49 | }; 50 | } 51 | 52 | onChangeText = (value) => { 53 | const { 54 | max, 55 | min, 56 | step, 57 | onNumberChanged, 58 | } = this.props; 59 | const sanitizedValue = Number(value); 60 | if (isNaN(sanitizedValue)) { 61 | return; 62 | } 63 | if (step && sanitizedValue % step !== 0) { 64 | return; 65 | } 66 | if (min && sanitizedValue < min) { 67 | return; 68 | } 69 | if (max && sanitizedValue > max) { 70 | return; 71 | } 72 | this.setState({ 73 | value: sanitizedValue, 74 | }, () => { 75 | onNumberChanged(sanitizedValue); 76 | }); 77 | }; 78 | 79 | update = (increment) => { 80 | const { 81 | max, 82 | min, 83 | step, 84 | onNumberChanged, 85 | } = this.props; 86 | const { value } = this.state; 87 | let finalValue; 88 | if (increment) { 89 | finalValue = step ? value + step : value + 1; 90 | if (max && finalValue > max) { 91 | finalValue = value; 92 | } 93 | } else { 94 | finalValue = step ? value - step : value - 1; 95 | if ((min || min === 0) && finalValue < min) { 96 | finalValue = value; 97 | } 98 | } 99 | this.setState({ 100 | value: finalValue, 101 | }, () => { 102 | onNumberChanged(finalValue); 103 | }); 104 | }; 105 | 106 | render() { 107 | const { 108 | label, 109 | value, 110 | error, 111 | placeholder, 112 | disabled, 113 | directTextEdit, 114 | } = this.props; 115 | return ( 116 | 117 | 121 | 122 | 123 | { 127 | this.update(false); 128 | }} 129 | > 130 | 135 | 136 | 137 | 146 | 147 | { 151 | this.update(true); 152 | }} 153 | > 154 | 159 | 160 | 161 | 162 | 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/components/CustomInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | View, 4 | TextInput, 5 | Text, 6 | } from 'react-native'; 7 | import PropTypes from 'prop-types'; 8 | import Icon from 'react-native-vector-icons/MaterialIcons'; 9 | 10 | import LabelError from '../LabelError'; 11 | 12 | import createStyle from './styles'; 13 | 14 | export default class CustomInput extends PureComponent { 15 | static propTypes = { 16 | disabled: PropTypes.bool, 17 | showBorder: PropTypes.bool, 18 | fontSize: PropTypes.number, 19 | icon: PropTypes.string, 20 | onChangeText: PropTypes.func, 21 | onChangeTextWithNewValue: PropTypes.func, 22 | validation: PropTypes.func, 23 | style: PropTypes.any, 24 | value: PropTypes.any, 25 | masked: PropTypes.bool, 26 | label: PropTypes.string, 27 | multiline: PropTypes.bool, 28 | error: PropTypes.bool, 29 | password: PropTypes.bool, 30 | placeholder: PropTypes.string, 31 | }; 32 | 33 | static defaultProps = { 34 | disabled: false, 35 | icon: '', 36 | onChangeText: null, 37 | onChangeTextWithNewValue: null, 38 | validation: () => {}, 39 | style: {}, 40 | value: null, 41 | masked: false, 42 | showBorder: true, 43 | fontSize: 16, 44 | label: '', 45 | multiline: false, 46 | error: false, 47 | password: false, 48 | placeholder: '', 49 | }; 50 | 51 | static contextTypes = { 52 | theme: PropTypes.object.isRequired, 53 | }; 54 | 55 | state = { 56 | multiLineStyle: {}, 57 | }; 58 | 59 | getValue = () => { 60 | return this.state.value; 61 | }; 62 | 63 | setValue = (value) => { 64 | this.setState({ value }); 65 | }; 66 | 67 | isValid = () => { 68 | return this.props.validation(this.getValue()); 69 | }; 70 | 71 | clear = () => { 72 | this.setState({ value: '' }); 73 | this.clearError(); 74 | }; 75 | 76 | clearError = () => { 77 | this.setState({ error: false }); 78 | }; 79 | 80 | calculateInputHeight = (event) => { 81 | if (this.props.multiline) { 82 | this.setState({ 83 | multiLineStyle: { 84 | height: event.nativeEvent.contentSize.height, 85 | }, 86 | }); 87 | } 88 | }; 89 | 90 | render() { 91 | const styles = createStyle( 92 | this.props.fontSize, 93 | this.state.error, 94 | this.props.icon, 95 | this.props.disabled, 96 | this.props.showBorder, 97 | this.props.multiline, 98 | this.props.error, 99 | ); 100 | 101 | // extract colors from theme 102 | const { theme } = this.context; 103 | const { 104 | textPrimary, 105 | error, 106 | iconDark, 107 | placeholderTextColor, 108 | } = theme.colors; 109 | 110 | return ( 111 | 112 | 116 | { 120 | let value; 121 | if (this.props.onChangeTextWithNewValue) { 122 | value = this.props.onChangeTextWithNewValue(input); 123 | } else { 124 | value = input; 125 | } 126 | this.setState({ value }, () => { 127 | if (this.state.error) { 128 | this.setState({ 129 | error: !this.isValid(), 130 | }); 131 | } 132 | }); 133 | if (this.props.onChangeText) { 134 | this.props.onChangeText(value); 135 | } 136 | }} 137 | onContentSizeChange={this.calculateInputHeight} 138 | placeholder={this.props.placeholder} 139 | placeholderTextColor={ 140 | this.state.error ? error : (theme.input.placeholderTextColor || placeholderTextColor) 141 | } 142 | selectionColor={textPrimary} 143 | style={[ 144 | styles.inputStyle, 145 | this.props.style, 146 | this.state.multiLineStyle, 147 | theme.input.style, 148 | ]} 149 | underlineColorAndroid="transparent" 150 | value={this.props.masked ? this.props.value : this.state.value || this.props.value} 151 | secureTextEntry={this.props.password} 152 | /> 153 | { 154 | !!this.props.icon 155 | && 156 | 157 | 162 | 163 | } 164 | 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /config/styles.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import _ from 'lodash'; 3 | 4 | import { 5 | primary, 6 | primaryDark, 7 | textPrimary, 8 | error, 9 | iconDark, 10 | textInputBorderColor, 11 | placeholderTextColor, 12 | starFillColor, 13 | black, 14 | white, 15 | success, 16 | } from './colors'; 17 | 18 | const fontFamily = Platform.OS === 'android' ? 'Roboto' : 'system font'; 19 | 20 | export const defaultColors = { 21 | primary, 22 | textPrimary, 23 | primaryDark, 24 | error, 25 | iconDark, 26 | textInputBorderColor, 27 | placeholderTextColor, 28 | starFillColor, 29 | black, 30 | white, 31 | success, 32 | }; 33 | 34 | export const defaultFonts = { 35 | defaultFontFamily: fontFamily, 36 | }; 37 | 38 | export const defaultTheme = { 39 | // labels 40 | label: { 41 | marginTop: 10, 42 | fontSize: 14, 43 | color: textPrimary, 44 | }, 45 | // error 46 | error: { 47 | fontSize: 12, 48 | color: error, 49 | }, 50 | // headers 51 | headers: { 52 | h1: { 53 | fontSize: 24, 54 | color: textPrimary, 55 | fontFamily, 56 | }, 57 | h2: { 58 | fontSize: 20, 59 | color: textPrimary, 60 | fontFamily, 61 | }, 62 | h3: { 63 | fontSize: 16, 64 | color: textPrimary, 65 | fontFamily, 66 | }, 67 | }, 68 | // paragraph 69 | p: { 70 | fontSize: 16, 71 | color: textPrimary, 72 | }, 73 | // input 74 | input: { 75 | placeholderTextColor, 76 | iconColor: iconDark, 77 | style: {}, 78 | }, 79 | // rating 80 | rating: { 81 | starFillColor, 82 | remarkStyle: { 83 | color: starFillColor, 84 | fontSize: 14, 85 | }, 86 | }, 87 | // toggle 88 | toggle: { 89 | knobColor: primaryDark, 90 | tintColor: primary, 91 | }, 92 | // select 93 | select: { 94 | tagRemoveIconColor: error, 95 | tagBorderColor: textInputBorderColor, 96 | tagTextColor: primary, 97 | selectedItemTextColor: primary, 98 | selectedItemIconColor: primary, 99 | itemTextColor: textPrimary, 100 | submitButtonColor: success, 101 | }, 102 | }; 103 | 104 | export const buildTheme = (userColors = {}, userFonts = {}, userTheme = {}) => { 105 | // merge colors 106 | const mergedColors = { 107 | ...defaultColors, 108 | ...userColors, 109 | }; 110 | // merge fonts 111 | const mergedFonts = { 112 | ...defaultFonts, 113 | ...userFonts, 114 | }; 115 | 116 | return { 117 | colors: mergedColors, 118 | fonts: mergedFonts, 119 | 120 | // labels 121 | label: { 122 | marginTop: 10, 123 | fontSize: 14, 124 | color: mergedColors.textPrimary, 125 | ...(_.get(userTheme, 'label')), 126 | }, 127 | // error 128 | error: { 129 | fontSize: 12, 130 | color: mergedColors.error, 131 | ...(_.get(userTheme, 'error')), 132 | }, 133 | // component related theme 134 | // headers 135 | headers: { 136 | h1: { 137 | fontSize: 28, 138 | color: mergedColors.textPrimary, 139 | fontFamily: mergedFonts.defaultFontFamily, 140 | ...(_.get(userTheme, 'headers.h1')), 141 | }, 142 | h2: { 143 | fontSize: 24, 144 | color: mergedColors.textPrimary, 145 | fontFamily: mergedFonts.defaultFontFamily, 146 | ...(_.get(userTheme, 'headers.h2')), 147 | }, 148 | h3: { 149 | fontSize: 20, 150 | color: mergedColors.textPrimary, 151 | fontFamily: mergedFonts.defaultFontFamily, 152 | ...(_.get(userTheme, 'headers.h3')), 153 | }, 154 | }, 155 | // paragraph 156 | p: { 157 | fontSize: 16, 158 | color: mergedColors.textPrimary, 159 | ...(_.get(userTheme, 'p')), 160 | }, 161 | // input 162 | input: { 163 | placeholderTextColor: mergedColors.placeholderTextColor, 164 | iconColor: mergedColors.iconDark, 165 | ...(_.get(userTheme, 'input')), 166 | }, 167 | // rating 168 | rating: { 169 | starFillColor: mergedColors.starFillColor, 170 | remarkStyle: { 171 | color: mergedColors.starFillColor, 172 | fontSize: 14, 173 | }, 174 | ...(_.get(userTheme, 'rating')), 175 | }, 176 | // toggle 177 | toggle: { 178 | knobColor: mergedColors.primaryDark, 179 | tintColor: mergedColors.primary, 180 | ...(_.get(userTheme, 'toggle')), 181 | }, 182 | // select 183 | select: { 184 | tagRemoveIconColor: mergedColors.error, 185 | tagBorderColor: mergedColors.primary, 186 | tagTextColor: mergedColors.primaryDark, 187 | selectedItemTextColor: mergedColors.primary, 188 | selectedItemIconColor: mergedColors.primary, 189 | itemTextColor: mergedColors.textPrimary, 190 | submitButtonColor: mergedColors.success, 191 | ...(_.get(userTheme, 'select')), 192 | }, 193 | }; 194 | }; 195 | 196 | export default {}; 197 | -------------------------------------------------------------------------------- /lib/components/CheckboxGroup/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | Switch, 6 | } from 'react-native'; 7 | import PropTypes from 'prop-types'; 8 | import { Checkbox } from 'react-native-material-ui'; 9 | import _ from 'lodash'; 10 | 11 | import LabelError from '../LabelError'; 12 | import CustomInput from '../CustomInput'; 13 | 14 | import styles from './styles'; 15 | 16 | export default class CheckboxGroup extends PureComponent { 17 | static propTypes = { 18 | label: PropTypes.string, 19 | options: PropTypes.array.isRequired, 20 | onCheckboxValueChanged: PropTypes.func, 21 | value: PropTypes.any, 22 | other: PropTypes.bool, 23 | toggle: PropTypes.bool, 24 | error: PropTypes.bool, 25 | }; 26 | 27 | static defaultProps = { 28 | label: '', 29 | onCheckboxValueChanged: () => { }, 30 | other: false, 31 | toggle: false, 32 | value: { 33 | regular: [], 34 | }, 35 | error: false, 36 | }; 37 | 38 | static contextTypes = { 39 | theme: PropTypes.object.isRequired, 40 | }; 41 | 42 | constructor(props) { 43 | super(props); 44 | this.state = { 45 | selectedValues: this.props.value, 46 | }; 47 | } 48 | 49 | onOtherTextChanged = (text) => { 50 | const { onCheckboxValueChanged } = this.props; 51 | const { selectedValues } = this.state; 52 | const clonedValues = _.cloneDeep(selectedValues); 53 | clonedValues.other.value = text; 54 | this.setState({ 55 | selectedValues: clonedValues, 56 | }, () => { 57 | onCheckboxValueChanged(clonedValues); 58 | }); 59 | }; 60 | 61 | onCheckChanged = (value, checked) => { 62 | const { onCheckboxValueChanged } = this.props; 63 | const { selectedValues } = this.state; 64 | const clonedValues = _.cloneDeep(selectedValues); 65 | if (checked) { 66 | if (value === 'other') { 67 | clonedValues.other = { 68 | value: '', 69 | }; 70 | } else { 71 | clonedValues.regular.push(value); 72 | } 73 | } else if (value === 'other') { 74 | // remove other field from state 75 | delete clonedValues.other; 76 | } else { 77 | // remove selected item from regular 78 | const index = clonedValues.regular.indexOf(value); 79 | if (index !== -1) { 80 | clonedValues.regular.splice(index, 1); 81 | } 82 | } 83 | this.setState({ 84 | selectedValues: clonedValues, 85 | }, () => { 86 | onCheckboxValueChanged(clonedValues); 87 | }); 88 | }; 89 | 90 | renderOtherInput = () => { 91 | const { selectedValues } = this.state; 92 | if (selectedValues.other) { 93 | return ( 94 | v} 97 | onChangeText={this.onOtherTextChanged} 98 | /> 99 | ); 100 | } 101 | return null; 102 | }; 103 | 104 | render() { 105 | const { 106 | label, 107 | options, 108 | other, 109 | toggle, 110 | error, 111 | } = this.props; 112 | const { theme } = this.context; 113 | const propsValue = this.props.value; 114 | return ( 115 | 116 | 120 | 121 | { 122 | _.map(options, value => ( 123 | toggle 124 | ? 125 | 134 | { 136 | this.onCheckChanged(_.get(value, 'value'), checked); 137 | }} 138 | thumbTintColor={theme.toggle.knobColor} 139 | onTintColor={theme.toggle.tintColor} 140 | value={propsValue.regular.indexOf(_.get(value, 'value')) !== -1} 141 | /> 142 | 143 | {_.get(value, 'label')} 144 | 145 | 146 | : 147 | 148 | { 153 | this.onCheckChanged(_.get(value, 'value'), checked); 154 | }} 155 | /> 156 | 157 | )) 158 | } 159 | { 160 | other 161 | ? 162 | 163 | { 164 | toggle 165 | ? 166 | 167 | { 171 | this.onCheckChanged('other', checked); 172 | }} 173 | value={!!propsValue.other} 174 | /> 175 | 176 | Other 177 | 178 | 179 | : 180 | { 185 | this.onCheckChanged('other', checked); 186 | }} 187 | /> 188 | } 189 | 190 | {this.renderOtherInput()} 191 | 192 | 193 | : 194 | null 195 | } 196 | 197 | 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /lib/DynamicForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ScrollView, 5 | } from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | import { ThemeProvider } from 'react-native-material-ui'; 8 | import _ from 'lodash'; 9 | 10 | import Header from '../components/Header'; 11 | import Paragraph from '../components/Paragraph'; 12 | import CustomInput from '../components/CustomInput'; 13 | import Rating from '../components/Rating'; 14 | import RadioGroup from '../components/RadioGroup'; 15 | import CheckboxGroup from '../components/CheckboxGroup'; 16 | import CustomDate from '../components/CustomDate'; 17 | import NumberSelector from '../components/NumberSelector'; 18 | import Select from '../components/Select'; 19 | import Button from '../components/Button'; 20 | 21 | import { getInputType } from '../util'; 22 | import { buildTheme } from '../../config/styles'; 23 | import styles from './styles'; 24 | 25 | const uiTheme = {}; 26 | const defaultTheme = buildTheme(); 27 | 28 | export default class DynamicForm extends Component { 29 | static propTypes = { 30 | form: PropTypes.array.isRequired, 31 | style: PropTypes.oneOfType([ 32 | PropTypes.object, 33 | PropTypes.number, 34 | ]), 35 | theme: PropTypes.object, 36 | onFormDataChange: PropTypes.func, 37 | submitButton: PropTypes.object, 38 | }; 39 | 40 | static defaultProps = { 41 | style: {}, 42 | theme: defaultTheme, 43 | onFormDataChange: () => {}, 44 | submitButton: {}, 45 | }; 46 | 47 | static childContextTypes = { 48 | theme: PropTypes.object.isRequired, 49 | }; 50 | 51 | constructor(props) { 52 | super(props); 53 | this.state = { 54 | responses: this.poupulateDefaultFormFields(), 55 | }; 56 | } 57 | 58 | getChildContext() { 59 | return { 60 | theme: this.props.theme, 61 | }; 62 | } 63 | 64 | getFormElementLabel = item => ( 65 | `${item.label} ${item.required ? '*' : ''}` 66 | ); 67 | 68 | getFormElementValue = (key, element) => { 69 | const { responses } = this.state; 70 | const formAnswer = _.get(responses, key); 71 | if (!_.isEmpty(formAnswer)) { 72 | return _.get(formAnswer, 'userAnswer'); 73 | } 74 | return _.get(element, 'value'); 75 | }; 76 | 77 | _getFormResponses = () => ( 78 | this.state.responses 79 | ); 80 | 81 | poupulateDefaultFormFields = () => { 82 | const { form } = this.props; 83 | // empty responses object 84 | const responses = {}; 85 | // loop through form to populate default values 86 | _.each(form, (element) => { 87 | switch (element.type) { 88 | case 'radio-group': 89 | const selectedValue = _.find(element.values, value => (value.selected)); 90 | if (selectedValue) { 91 | _.set( 92 | responses, 93 | element.key, 94 | { 95 | ...element, 96 | userAnswer: selectedValue.value, 97 | }, 98 | ); 99 | } 100 | break; 101 | case 'starRating': 102 | case 'number': 103 | case 'date': 104 | case 'text': 105 | case 'textarea': 106 | if (element.value) { 107 | _.set( 108 | responses, 109 | element.key, 110 | { 111 | ...element, 112 | userAnswer: element.value, 113 | }, 114 | ); 115 | } 116 | break; 117 | case 'select': 118 | const selectedValues = _.filter(element.values, value => (value.selected)); 119 | if (!_.isEmpty(selectedValues)) { 120 | _.set( 121 | responses, 122 | element.key, 123 | { 124 | ...element, 125 | userAnswer: [], 126 | }, 127 | ); 128 | _.each(selectedValues, (value) => { 129 | responses[element.key].userAnswer.push(value.value); 130 | }); 131 | } 132 | break; 133 | case 'checkbox-group': 134 | const valuesSelected = _.filter(element.values, value => (value.selected)); 135 | if (!_.isEmpty(selectedValues)) { 136 | _.set( 137 | responses, 138 | element.key, 139 | { 140 | ...element, 141 | userAnswer: { 142 | regular: [], 143 | }, 144 | }, 145 | ); 146 | _.each(valuesSelected, (value) => { 147 | responses[element.key].userAnswer.regular.push(value.value); 148 | }); 149 | } 150 | break; 151 | default: 152 | break; 153 | } 154 | }); 155 | return responses; 156 | }; 157 | 158 | updateFormElement = (value, element) => { 159 | const { onFormDataChange } = this.props; 160 | const { responses } = this.state; 161 | const clonedResponses = _.cloneDeep(responses); 162 | const formAnswer = _.get(clonedResponses, element.key); 163 | if (!_.isEmpty(formAnswer)) { 164 | formAnswer.userAnswer = value; 165 | } else { 166 | _.set( 167 | clonedResponses, 168 | element.key, 169 | { 170 | ...element, 171 | userAnswer: value, 172 | }, 173 | ); 174 | } 175 | console.log('Data changes...'); 176 | this.setState({ 177 | responses: clonedResponses, 178 | }, () => { 179 | // if listener is set on form data changes, 180 | // propagate back to parent component 181 | if (onFormDataChange) { 182 | onFormDataChange(this.state.responses); 183 | } 184 | }); 185 | }; 186 | 187 | submitButtonPress = () => { 188 | const { action } = this.props.submitButton; 189 | action(this.state.responses); 190 | } 191 | 192 | renderForm = () => { 193 | const { form, submitButton } = this.props; 194 | const formElements = _.map(form, (element) => { 195 | const { 196 | key, 197 | type, 198 | subtype, 199 | style, 200 | } = element; 201 | const label = this.getFormElementLabel(element); 202 | // const hasError = errors.indexOf(key) !== -1 && this.submitTriggered; 203 | switch (type) { 204 | case 'header': 205 | return ( 206 | 207 |
212 | 213 | ); 214 | case 'paragraph': 215 | return ( 216 | 217 | 221 | 222 | ); 223 | case 'text': 224 | const moreOptions = {}; 225 | if (element.maxlength) { 226 | moreOptions.maxLength = Number(element.maxlength); 227 | } 228 | return ( 229 | 230 | { 233 | this.updateFormElement(value, element); 234 | }} 235 | value={this.getFormElementValue(key, element)} 236 | label={label} 237 | keyboardType={getInputType(element.subtype)} 238 | validation={element.validationFunc} 239 | password={element.subtype === 'password'} 240 | placeholder={element.placeholder} 241 | disabled={element.disabled} 242 | icon={element.icon} 243 | masked 244 | /> 245 | 246 | ); 247 | case 'textarea': 248 | const textAreaOptions = {}; 249 | if (element.maxlength) { 250 | textAreaOptions.maxLength = Number(element.maxlength); 251 | } 252 | return ( 253 | 254 | { 257 | this.updateFormElement(value, element); 258 | }} 259 | multiline 260 | value={this.getFormElementValue(key, element)} 261 | label={label} 262 | keyboardType="default" 263 | validation={element.validationFunc} 264 | placeholder={element.placeholder} 265 | disabled={element.disabled} 266 | /> 267 | 268 | ); 269 | case 'starRating': 270 | return ( 271 | 272 | { 274 | const maxStars = element.maxStars || 5; 275 | const starCount = Number(this.getFormElementValue(key, element)); 276 | if (isNaN(starCount)) { 277 | return 0; 278 | } 279 | return starCount > maxStars ? 0 : starCount; 280 | })()} 281 | label={label} 282 | onStarRatingChange={(starCount) => { 283 | this.updateFormElement(starCount, element); 284 | }} 285 | maxStars={element.maxStars} 286 | config={element.config} 287 | /> 288 | 289 | ); 290 | case 'radio-group': 291 | const radioOptions = {}; 292 | if (element.other) { 293 | radioOptions.other = element.other; 294 | } 295 | return ( 296 | 297 | { 303 | this.updateFormElement(value, element); 304 | }} 305 | /> 306 | 307 | ); 308 | case 'checkbox-group': 309 | const checkOptions = {}; 310 | if (element.other) { 311 | checkOptions.other = element.other; 312 | } 313 | if (element.toggle) { 314 | checkOptions.toggle = element.toggle; 315 | } 316 | return ( 317 | 318 | { 323 | this.updateFormElement(value, element); 324 | }} 325 | value={this.getFormElementValue(key, element)} 326 | /> 327 | 328 | ); 329 | case 'date': 330 | return ( 331 | 332 | { 337 | this.updateFormElement(value, element); 338 | }} 339 | disabled={element.disabled} 340 | minDate={element.minDate} 341 | maxDate={element.maxDate} 342 | dateFormat={element.dateFormat} 343 | /> 344 | 345 | ); 346 | case 'select': 347 | const multiOptions = { 348 | single: !element.multiple, 349 | }; 350 | return ( 351 | 352 |