├── 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 |
363 | );
364 | case 'number':
365 | return (
366 |
367 | {
372 | this.updateFormElement(value, element);
373 | }}
374 | min={element.min}
375 | max={element.max}
376 | step={element.step}
377 | directTextEdit={element.directTextEdit}
378 | value={(() => {
379 | const number = Number(this.getFormElementValue(key, element));
380 | if (isNaN(number)) {
381 | return element.min;
382 | }
383 | if (number < element.min) {
384 | return element.min;
385 | }
386 | if (number > element.max) {
387 | return element.max;
388 | }
389 | return number;
390 | })()}
391 | />
392 |
393 | );
394 | default:
395 | return null;
396 | }
397 | });
398 | // push submit button if specified
399 | if (!_.isEmpty(submitButton)) {
400 | const {
401 | label,
402 | buttonStyle,
403 | buttonTextStyle,
404 | disabled,
405 | } = submitButton;
406 | formElements.push(
407 |
411 |
418 | );
419 | }
420 | return formElements;
421 | };
422 |
423 | render() {
424 | return (
425 |
426 |
427 |
430 | {this.renderForm()}
431 |
432 |
433 |
434 | );
435 | }
436 | }
437 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-dynamic-form
2 |
3 | [](https://www.npmjs.com/package/react-native-dynamic-form) [](https://www.npmjs.com/package/react-native-dynamic-form) [](https://www.npmjs.com/package/react-native-dynamic-form)
4 |
5 | > Dynamic form builder for React Native
6 |
7 | ### Sneak Peak
8 |
9 |
10 |
11 | ## Installation
12 |
13 | ``` bash
14 | $ npm install react-native-dynamic-form --save
15 | ```
16 | or use yarn
17 |
18 | ``` bash
19 | $ yarn add react-native-dynamic-form
20 | ```
21 |
22 | ## Usage
23 | Note: Ensure to add and configure [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) in your project before using this package.
24 |
25 | You can clone and try out the [sample](https://github.com/toystars/dynamic_form_sample) app.
26 |
27 | Sample usage:
28 |
29 | ``` javascript
30 | import React, { Component } from 'react';
31 | import {
32 | StyleSheet,
33 | View
34 | } from 'react-native';
35 | import DynamicForm from 'react-native-dynamic-form';
36 |
37 | const form = [
38 | {
39 | key: 'hdghhdbdfgh',
40 | type: 'header',
41 | subtype: 'h1',
42 | label: 'Dynamic Form',
43 | },
44 | ];
45 |
46 | export default class App extends Component {
47 |
48 | render() {
49 | return (
50 |
51 |
55 |
56 | );
57 | }
58 | }
59 |
60 | const styles = StyleSheet.create({
61 | container: {
62 | flex: 1,
63 | backgroundColor: '#F5FCFF',
64 | },
65 | formContainer: {
66 | marginTop: 10,
67 | },
68 | });
69 |
70 | ```
71 |
72 | ## Props
73 |
74 | The component takes one compulsory prop - `form`. Other props are optional. The table below explains more.
75 |
76 | | Prop | Required | Type | Purpose |
77 | | ------------|-----------| -------| -------|
78 | | form | Yes | Array | array of objects representing form components to render (see Form Components for more info) |
79 | | theme | No | Object | Theme to apply to entire dynamic form elements. See below for more info |
80 | | style | No | Object, Number | Style to apply to form container. View Style |
81 | | onFormDataChange | No | Function | Returns form responses as funtion argument whenever any change occur in form |
82 | | submitButton | No | Object | Object for displaying button at the end of form. More info below |
83 |
84 |
85 | ### submitButton (prop)
86 |
87 | Object for displaying button at the end of form. Holds configuration on button appeareance and beahaviour. See below for sample:
88 |
89 | ``` javascript
90 | import React, { Component } from 'react';
91 | import { View } from 'react-native';
92 | import DynamicForm, { buildTheme } from 'react-native-dynamic-form';
93 |
94 | const theme = buildTheme();
95 |
96 | export default class App extends Component {
97 | render() {
98 | return (
99 |
100 | {
105 | console.log('Submit Button Responses: ', responses);
106 | }, // button action, takes form responses as argument, optional
107 | label: 'Submit', // button label, required
108 | buttonStyle: {
109 | backgroundColor: '#CCCCCC',
110 | borderRadius: 3,
111 | height: 40,
112 | }, // View PropTypes, optional
113 | buttonTextStyle: {
114 | fontSiz3: 18,
115 | }, // Text PropTypes, optional
116 | disabled: false, // boolean to disable button, optional
117 | }}
118 | />
119 |
120 | );
121 | }
122 | }
123 | ```
124 |
125 | ### _getFormResponses
126 |
127 | Use component reference to get form responses at any point in time
128 |
129 | ``` javascript
130 | import React, { Component } from 'react';
131 | import { View, Button } from 'react-native';
132 | import DynamicForm, { buildTheme } from 'react-native-dynamic-form';
133 |
134 |
135 | export default class App extends Component {
136 |
137 | getFormResponses = () => {
138 | const responses = this.formRef._getFormResponses();
139 | // use responses here...
140 | }
141 |
142 | render() {
143 | return (
144 |
145 | this.formRef = ref}
147 | form={form}
148 | />
149 |
153 |
154 | );
155 | }
156 | }
157 | ```
158 |
159 | ## Theming
160 |
161 | A global theme object can be passed to the `Dynamic Form` component which gets applied to most of the form elements. Theming is still a work in progress and far from perfect, but it provides a basic way of customizing form elements. Theming instructions are provided below:
162 |
163 | ``` javascript
164 | import React, { Component } from 'react';
165 | import { View } from 'react-native';
166 | import { buildTheme } from 'react-native-dynamic-form';
167 |
168 | const theme = buildTheme();
169 |
170 | export default class App extends Component {
171 | render() {
172 | return (
173 |
174 |
178 |
179 | );
180 | }
181 | }
182 | ```
183 |
184 | The code sample above builds a theme template and apply defaults configuration. This [file](config/styles.js) conatins the default values. The `defaultTheme` export in the styles config can be used as a template to build really customized theme object.
185 |
186 | ### Full customization
187 |
188 | To fully customize form elements, the `buildTheme` export accepts three parameters in the order they appear below:
189 |
190 | - **userColors** - Object of colors to apply to form element. See [file](config/styles.js) for exact format.
191 |
192 | - **userFonts** - Object of fonts to apply to form element. See [file](config/styles.js) for exact format.
193 |
194 | - **userTheme** - Complete user theme object which is a clone of `defaultTheme` export in [styles config](config/styles.js) file. This can be copied over and modified.
195 |
196 | Below is an example of a fully customized theme builder
197 |
198 | ``` javascript
199 | import { buildTheme } from 'react-native-dynamic-form';
200 |
201 | const myColors = {
202 | primary: '#00a5ff',
203 | textPrimary: '#2A3C53',
204 | primaryDark: '#0077cb',
205 | error: '#FF6565',
206 | iconDark: 'rgba(0,0,0,0.4)',
207 | textInputBorderColor: '#cac8c8',
208 | placeholderTextColor: '#A9A9A9',
209 | starFillColor: '#f5a623',
210 | black: '#000000',
211 | white: '#FFFFFF',
212 | success: '#50e3c2',
213 | };
214 |
215 | const myFonts = {
216 | defaultFontFamily: 'Roboto', // font should already be added to project
217 | };
218 |
219 | const myTheme = {
220 | // labels
221 | label: {
222 | marginTop: 10,
223 | fontSize: 14,
224 | color: myColors.textPrimary,
225 | },
226 | // error
227 | error: {
228 | fontSize: 12,
229 | color: myColors.error,
230 | },
231 | // headers
232 | headers: {
233 | h1: {
234 | fontSize: 24,
235 | color: myColors.textPrimary,
236 | fontFamily: myFonts.defaultFontFamily,
237 | },
238 | h2: {
239 | fontSize: 20,
240 | color: myColors.textPrimary,
241 | fontFamily: myFonts.defaultFontFamily,
242 | },
243 | h3: {
244 | fontSize: 16,
245 | color: myColors.textPrimary,
246 | fontFamily: myFonts.defaultFontFamily,
247 | },
248 | },
249 | // paragraph
250 | p: {
251 | fontSize: 16,
252 | color: myColors.textPrimary,
253 | fontFamily: myFonts.defaultFontFamily,
254 | },
255 | // input
256 | input: {
257 | placeholderTextColor,
258 | iconColor: iconDark,
259 | style: {},
260 | },
261 | // rating
262 | rating: {
263 | starFillColor,
264 | remarkStyle: {
265 | color: myColors.starFillColor,
266 | fontSize: 14,
267 | },
268 | },
269 | // toggle
270 | toggle: {
271 | knobColor: myColors.primaryDark,
272 | tintColor: myColors.primary,
273 | },
274 | // select
275 | select: {
276 | tagRemoveIconColor: myColors.error,
277 | tagBorderColor: myColors.textInputBorderColor,
278 | tagTextColor: myColors.primary,
279 | selectedItemTextColor: myColors.primary,
280 | selectedItemIconColor: myColors.primary,
281 | itemTextColor: myColors.textPrimary,
282 | submitButtonColor: myColors.success,
283 | },
284 | };
285 |
286 | const theme = buildTheme(myColors, myFonts, myTheme);
287 |
288 | // The most important paramater is the colors parameter which will be applied to all form elements.
289 | // Other sections of the theme object will be set to their default values and provided colors applied where necessary
290 | // Most times you will only have to do as below and leave the remaining configuration in their default state
291 | const theme = buildTheme(myColors);
292 | ```
293 |
294 |
295 | ## Form Components
296 |
297 | All component fields are required, except stated otherwise.
298 |
299 | ### Header
300 |
301 | Represents a header component.
302 |
303 |
304 |
305 | ``` javascript
306 | {
307 | key: 'hdghhdbdfgh',
308 | type: 'header',
309 | subtype: 'h2', // one of h1, h2 and h3
310 | label: 'Dynamic Form',
311 | style: {
312 | fontSize: 14,
313 | }, // optional
314 | }
315 | ```
316 |
317 | ### Paragraph
318 |
319 | Represents a paragraph component.
320 |
321 |
322 |
323 |
324 | ``` javascript
325 | {
326 | key: 'addsdfdvdvdd',
327 | type: 'paragraph',
328 | label: 'Instructions on how to fill dynamic form',
329 | style: {
330 | fontSize: 15,
331 | }, // optional
332 | }
333 | ```
334 |
335 | ### TextInput
336 |
337 | Represents an input component.
338 |
339 |
340 |
341 | ``` javascript
342 | {
343 | key: 'manshgdsuudfg',
344 | type: "text",
345 | required: true, // optional
346 | label: 'What is your last name?',
347 | placeholder: 'Last Name', // optional
348 | subtype: 'text', // one of text, tel, email and password
349 | maxlength: 30, // optional
350 | value: 'Salako', // optional
351 | disabled: false, // optional,
352 | icon: 'lock', // optional
353 | validationFunc: (value) => {
354 | // do validation here and return bool status
355 | }, // optional
356 | }
357 | ```
358 |
359 | ### TextArea
360 |
361 | Represents a multi-line input component.
362 |
363 |
364 |
365 | ``` javascript
366 | {
367 | key: 'jahaughabdvad',
368 | type: 'textarea',
369 | label: 'Please describe yourself in not more than 400 characters',
370 | placeholder: "My name is John Doe and I am...", // optional
371 | maxlength: 400, // optional
372 | required: true, // optional
373 | value: '', // optional
374 | validationFunc: (value) => {
375 | // do validation here and return bool status
376 | }, // optional
377 | }
378 | ```
379 |
380 | ### Rating
381 |
382 | Represents a rating component.
383 |
384 |
385 |
386 | ``` javascript
387 | {
388 | key: 'liaksunshdfjnbah',
389 | type: 'starRating',
390 | label: 'Rate your programming skill',
391 | maxStars: 5,
392 | value: 3,
393 | required: false, // optional
394 | config: {
395 | iconSet: 'MaterialIcons', // react-native-vector-icon icon sets
396 | emptyStar: 'star-border', // empty star icon
397 | fullStar: 'star', // full star icon
398 | halfStar: 'star-half', // half star icon, if enableHalfStar is set to true
399 | enableHalfStar: false, // enable half star
400 | ratingRemark: {
401 | 1: 'Beginer',
402 | 2: 'Enthusiast',
403 | 3: 'Junior',
404 | 4: 'Intermediate',
405 | 5: 'Senior',
406 | },
407 | },
408 | }
409 | ```
410 |
411 | ### Radio Button
412 |
413 | Represents a radio button component.
414 |
415 |
416 |
417 | ``` javascript
418 | {
419 | key: 'sbasgdsbdgffgf',
420 | type: 'radio-group',
421 | label: 'Favorite Programming Language',
422 | other: true, // displays an other input for custom data entry, optional
423 | values: [
424 | {
425 | label: 'JavaScript',
426 | value: 'javascript',
427 | selected: true, // selected value (can be used to preselect values too) optional
428 | },
429 | {
430 | label: 'Ruby',
431 | value: 'ruby',
432 | }
433 | ]
434 | }
435 | ```
436 |
437 | ### CheckBox
438 |
439 | Represents a checkbox component.
440 |
441 |
442 |
443 | ``` javascript
444 | {
445 | key: 'avfsragsghgdbhfg',
446 | type: 'checkbox-group',
447 | label: 'Top 3 tech companies in the world',
448 | other: true, // optional
449 | values: [
450 | {
451 | label: 'Google',
452 | value: 'google',
453 | },
454 | {
455 | label: 'Apple',
456 | value: 'apple',
457 | },
458 | {
459 | label: 'Facebook',
460 | value: 'facebook',
461 | selected: true,
462 | },
463 | {
464 | label: 'Microsoft',
465 | value: 'microsoft',
466 | },
467 | {
468 | label: 'Amazon',
469 | value: 'amazon',
470 | },
471 | ]
472 | }
473 | ```
474 |
475 | ### Toggle
476 |
477 | Represents a toggle component.
478 |
479 |
480 |
481 | ``` javascript
482 | {
483 | key: 'anhsgabgbfnhhdnbf',
484 | type: 'checkbox-group',
485 | label: 'Sorting Algorithms familiar with',
486 | toggle: true, // renders a toggle instead of checkbox, optional
487 | other: true, // renders an other field for free text entry
488 | values: [
489 | {
490 | label: 'Bubble Sort',
491 | value: 'bubble sort',
492 | selected: true, // selected value (can be used to preselect values too) optional
493 | },
494 | {
495 | label: 'Heapsort',
496 | value: 'heapsort',
497 | },
498 | {
499 | label: 'Insertion Sort',
500 | value: 'insertion sort',
501 | },
502 | {
503 | label: 'Merge Sort',
504 | value: 'merge sort',
505 | },
506 | {
507 | label: 'Quicksort',
508 | value: 'quicksort',
509 | },
510 | ]
511 | },
512 | ```
513 |
514 | ### Date
515 |
516 | Represents a date component.
517 |
518 |
519 |
520 |
521 |
522 | ``` javascript
523 | {
524 | key: 'aabnstfavahbdaas',
525 | type: 'date',
526 | label: 'Date of Birth',
527 | value: '26-11-2018',
528 | placeholder: '25-05-2018', // optional
529 | dateFormat: 'DD-MM-YYYY', // optional
530 | disabled: false, // optional
531 | }
532 | ```
533 |
534 | ### Number Input
535 |
536 | Represents a number input component.
537 |
538 |
539 |
540 | ``` javascript
541 | {
542 | key: 'manbsgvagsdbdhh',
543 | type: 'number',
544 | label: 'Number of Data Structure and Algorithm books read',
545 | placeholder: 0, // optional
546 | value: 0,
547 | min: 0, // minimum allowed value, optional
548 | max: 100, // maximum allowed value, optional
549 | step: 2, // value step, optional, defaults to 1
550 | disabled: false, // optional
551 | directTextEdit: false, // enable or disable free form data entry, optional, defaults to false
552 | }
553 | ```
554 |
555 | ### Select
556 |
557 | Represents a select component.
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 | ``` javascript
568 | {
569 | key: 'nabsgsgdhyshdhf',
570 | type: 'select',
571 | label: 'Languages Spoken',
572 | multiple: false, // enable multiple selection and displays selected items as tags, optional
573 | searchInputPlaceholder: 'Search Languages...',
574 | values: [
575 | {
576 | label: 'Yoruba',
577 | value: 'yoruba',
578 | selected: true, // selected value (can be used to preselect values too) optional
579 | },
580 | {
581 | label: 'Igbo',
582 | value: 'igbo',
583 | },
584 | {
585 | label: 'Hausa',
586 | value: 'hausa',
587 | },
588 | {
589 | label: 'English',
590 | value: 'english',
591 | },
592 | {
593 | label: 'Spanish',
594 | value: 'spanish',
595 | },
596 | {
597 | label: 'French',
598 | value: 'french',
599 | },
600 | ]
601 | }
602 | ```
603 |
604 | ## Contributing
605 |
606 | Contributions are **welcome** and will be fully **credited**.
607 |
608 | Contributions are accepted via Pull Requests on [Github](https://github.com/toystars/react-native-dynamic-form).
609 |
610 |
611 | ### Pull Requests
612 |
613 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
614 |
615 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
616 |
617 | - **Create feature branches** - Don't ask us to pull from your master branch.
618 |
619 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
620 |
621 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
622 |
623 |
624 | ## Issues
625 |
626 | Check [issues](https://github.com/toystars/react-native-dynamic-form/issues) for current issues.
627 |
628 | ## Contributors
629 |
630 |
631 | ## License
632 |
633 | The MIT License (MIT). Please see [LICENSE](LICENSE) for more information.
634 |
635 |
--------------------------------------------------------------------------------