├── docs ├── images │ ├── error.png │ ├── help.png │ ├── result.png │ ├── validation.png │ ├── placeholders.png │ └── stylesheets │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ └── 7.png └── STYLESHEETS.md ├── .gitignore ├── .travis.yml ├── lib ├── index.js ├── i18n │ └── en.js ├── templates │ └── bootstrap │ │ ├── index.js │ │ ├── struct.js │ │ ├── checkbox.js │ │ ├── select.android.js │ │ ├── list.js │ │ ├── textbox.js │ │ ├── datepicker.ios.js │ │ ├── select.ios.js │ │ └── datepicker.android.js ├── util.js ├── stylesheets │ └── bootstrap.js └── components.js ├── .npmignore ├── index.js ├── ISSUE_TEMPLATE.md ├── LICENSE ├── .eslintrc ├── package.json ├── CONTRIBUTING.md ├── CHANGELOG.md ├── test └── index.js └── README.md /docs/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/error.png -------------------------------------------------------------------------------- /docs/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/help.png -------------------------------------------------------------------------------- /docs/images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/result.png -------------------------------------------------------------------------------- /docs/images/validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/validation.png -------------------------------------------------------------------------------- /docs/images/placeholders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/placeholders.png -------------------------------------------------------------------------------- /docs/images/stylesheets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/1.png -------------------------------------------------------------------------------- /docs/images/stylesheets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/2.png -------------------------------------------------------------------------------- /docs/images/stylesheets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/3.png -------------------------------------------------------------------------------- /docs/images/stylesheets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/4.png -------------------------------------------------------------------------------- /docs/images/stylesheets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/5.png -------------------------------------------------------------------------------- /docs/images/stylesheets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/6.png -------------------------------------------------------------------------------- /docs/images/stylesheets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeorgeBake/stephenfewerj/HEAD/docs/images/stylesheets/7.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | AwesomeProject.xcodeproj 4 | AwesomeProjectTests 5 | index.ios.js 6 | iOS 7 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.7.0" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var t = require("tcomb-validation"); 2 | var form = require("./components"); 3 | 4 | t.form = form; 5 | 6 | module.exports = t; 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | node_modules 4 | test 5 | docs 6 | AwesomeProject.xcodeproj 7 | AwesomeProjectTests 8 | index.ios.js 9 | iOS 10 | -------------------------------------------------------------------------------- /lib/i18n/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | optional: " (optional)", 3 | required: "", 4 | add: "Add", 5 | remove: "✘", 6 | up: "↑", 7 | down: "↓" 8 | }; 9 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | textbox: require("./textbox"), 3 | checkbox: require("./checkbox"), 4 | select: require("./select"), 5 | datepicker: require("./datepicker"), 6 | struct: require("./struct"), 7 | list: require("./list") 8 | }; 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import t from "./lib"; 2 | import i18n from "./lib/i18n/en"; 3 | import templates from "./lib/templates/bootstrap"; 4 | import stylesheet from "./lib/stylesheets/bootstrap"; 5 | 6 | t.form.Form.templates = templates; 7 | t.form.Form.stylesheet = stylesheet; 8 | t.form.Form.i18n = i18n; 9 | 10 | t.form.Form.defaultProps = { 11 | templates: t.form.Form.templates, 12 | stylesheet: t.form.Form.stylesheet, 13 | i18n: t.form.Form.i18n 14 | }; 15 | 16 | module.exports = t; 17 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Version 2 | 3 | Tell us which versions you are using: 4 | 5 | - tcomb-form-native v0.?.? 6 | - react-native v0.?.? 7 | 8 | ### Expected behaviour 9 | 10 | Tell us what should happen 11 | 12 | ### Actual behaviour 13 | 14 | Tell us what happens instead 15 | 16 | ### Steps to reproduce 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | ### Stack trace and console log 23 | 24 | Hint: it would help a lot if you enable the debugger ("Pause on exceptions" in the "Source" panel of Chrome dev tools) and spot the place where the error is thrown 25 | 26 | ``` 27 | ``` 28 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/struct.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var { View, Text } = require("react-native"); 3 | 4 | function struct(locals) { 5 | if (locals.hidden) { 6 | return null; 7 | } 8 | 9 | var stylesheet = locals.stylesheet; 10 | var fieldsetStyle = stylesheet.fieldset; 11 | var controlLabelStyle = stylesheet.controlLabel.normal; 12 | 13 | if (locals.hasError) { 14 | controlLabelStyle = stylesheet.controlLabel.error; 15 | } 16 | 17 | var label = locals.label ? ( 18 | {locals.label} 19 | ) : null; 20 | var error = 21 | locals.hasError && locals.error ? ( 22 | 23 | {locals.error} 24 | 25 | ) : null; 26 | 27 | var rows = locals.order.map(function(name) { 28 | return locals.inputs[name]; 29 | }); 30 | 31 | return ( 32 | 33 | {label} 34 | {error} 35 | {rows} 36 | 37 | ); 38 | } 39 | 40 | module.exports = struct; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Giulio Canti 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 | 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier", "prettier/react", "prettier/standard"], 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "plugins": ["react", "prettier"], 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "modules": true, 13 | "jsx": true 14 | } 15 | }, 16 | "rules": { 17 | "prettier/prettier": 1, 18 | "eol-last": 0, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 0, 21 | "no-unused-vars": 0, 22 | "no-shadow": 0, 23 | "no-multi-spaces": 0, 24 | "consistent-return": 0, 25 | "no-use-before-define": 0, 26 | "quotes": [2, "double"], 27 | "comma-dangle": 1, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-quotes": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-uses-react": 1, 32 | "react/jsx-uses-vars": 1, 33 | "react/no-did-mount-set-state": 1, 34 | "react/no-did-update-set-state": 1, 35 | "react/no-multi-comp": 1, 36 | "react/no-unknown-property": 1, 37 | "react/react-in-jsx-scope": 1, 38 | "react/self-closing-comp": 1, 39 | "react/wrap-multilines": 1 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcomb-form-native", 3 | "version": "0.6.14", 4 | "description": 5 | "react-native powered UI library for developing forms writing less code", 6 | "main": "index.js", 7 | "scripts": { 8 | "lint": "eslint lib", 9 | "test": "npm run lint && babel-node test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/gcanti/tcomb-form-native.git" 14 | }, 15 | "author": "Giulio Canti ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/gcanti/tcomb-form-native/issues" 19 | }, 20 | "homepage": "https://github.com/gcanti/tcomb-form-native", 21 | "dependencies": { 22 | "tcomb-validation": "^3.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/eslint-plugin-prettier": "^2.2.0", 26 | "@types/prettier": "^1.7.0", 27 | "babel": "5.8.34", 28 | "babel-core": "5.8.34", 29 | "eslint": "^4.19.1", 30 | "eslint-config-prettier": "^2.9.0", 31 | "eslint-plugin-prettier": "^2.6.0", 32 | "eslint-plugin-react": "2.5.2", 33 | "prettier": "^1.7.4", 34 | "prop-types": "^15.5.10", 35 | "react": "^15.6.1", 36 | "tape": "3.5.0" 37 | }, 38 | "tags": ["tcomb", "form", "forms", "react", "react-native", "react-component"], 39 | "keywords": [ 40 | "tcomb", 41 | "form", 42 | "forms", 43 | "react", 44 | "react-native", 45 | "react-component" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Contributions are welcome and are greatly appreciated! Every little bit helps, and credit will 4 | always be given. 5 | 6 | ## Issues Request Guidelines 7 | 8 | Before you submit an issue, check that it meets these guidelines: 9 | 10 | - specify the version of `tcomb-form-native` you are using 11 | - specify the version of `react-native` you are using 12 | - if the issue regards a bug, please provide a minimal failing example / test 13 | 14 | ## Pull Request Guidelines 15 | 16 | Before you submit a pull request from your forked repo, check that it meets these guidelines: 17 | 18 | 1. If the pull request fixes a bug, it should include tests that fail without the changes, and pass 19 | with them. 20 | 2. If the pull request adds functionality, the docs should be updated as part of the same PR. 21 | 3. Please rebase and resolve all conflicts before submitting. 22 | 23 | ## Setting up your environment 24 | 25 | After forking tcomb-form-native to your own github org, do the following steps to get started: 26 | 27 | ```sh 28 | # clone your fork to your local machine 29 | git clone https://github.com/gcanti/tcomb-form-native.git 30 | 31 | # step into local repo 32 | cd tcomb-form-native 33 | 34 | # install dependencies 35 | npm install 36 | ``` 37 | 38 | ### Running Tests 39 | 40 | ```sh 41 | npm test 42 | ``` 43 | 44 | ### Style & Linting 45 | 46 | This codebase adheres to a custom style and is 47 | enforced using [ESLint](http://eslint.org/). 48 | 49 | It is recommended that you install an eslint plugin for your editor of choice when working on this 50 | codebase, however you can always check to see if the source code is compliant by running: 51 | 52 | ```sh 53 | npm run lint 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/checkbox.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var { View, Text, Switch } = require("react-native"); 3 | 4 | function checkbox(locals) { 5 | if (locals.hidden) { 6 | return null; 7 | } 8 | 9 | var stylesheet = locals.stylesheet; 10 | var formGroupStyle = stylesheet.formGroup.normal; 11 | var controlLabelStyle = stylesheet.controlLabel.normal; 12 | var checkboxStyle = stylesheet.checkbox.normal; 13 | var helpBlockStyle = stylesheet.helpBlock.normal; 14 | var errorBlockStyle = stylesheet.errorBlock; 15 | 16 | if (locals.hasError) { 17 | formGroupStyle = stylesheet.formGroup.error; 18 | controlLabelStyle = stylesheet.controlLabel.error; 19 | checkboxStyle = stylesheet.checkbox.error; 20 | helpBlockStyle = stylesheet.helpBlock.error; 21 | } 22 | 23 | var label = locals.label ? ( 24 | {locals.label} 25 | ) : null; 26 | var help = locals.help ? ( 27 | {locals.help} 28 | ) : null; 29 | var error = 30 | locals.hasError && locals.error ? ( 31 | 32 | {locals.error} 33 | 34 | ) : null; 35 | 36 | return ( 37 | 38 | {label} 39 | locals.onChange(value)} 48 | value={locals.value} 49 | /> 50 | {help} 51 | {error} 52 | 53 | ); 54 | } 55 | 56 | module.exports = checkbox; 57 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/select.android.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var { View, Text, Picker } = require("react-native"); 3 | 4 | function select(locals) { 5 | if (locals.hidden) { 6 | return null; 7 | } 8 | 9 | var stylesheet = locals.stylesheet; 10 | var formGroupStyle = stylesheet.formGroup.normal; 11 | var controlLabelStyle = stylesheet.controlLabel.normal; 12 | var selectStyle = Object.assign( 13 | {}, 14 | stylesheet.select.normal, 15 | stylesheet.pickerContainer.normal 16 | ); 17 | var helpBlockStyle = stylesheet.helpBlock.normal; 18 | var errorBlockStyle = stylesheet.errorBlock; 19 | 20 | if (locals.hasError) { 21 | formGroupStyle = stylesheet.formGroup.error; 22 | controlLabelStyle = stylesheet.controlLabel.error; 23 | selectStyle = stylesheet.select.error; 24 | helpBlockStyle = stylesheet.helpBlock.error; 25 | } 26 | 27 | var label = locals.label ? ( 28 | {locals.label} 29 | ) : null; 30 | var help = locals.help ? ( 31 | {locals.help} 32 | ) : null; 33 | var error = 34 | locals.hasError && locals.error ? ( 35 | 36 | {locals.error} 37 | 38 | ) : null; 39 | 40 | var options = locals.options.map(({ value, text }) => ( 41 | 42 | )); 43 | 44 | return ( 45 | 46 | {label} 47 | 59 | {options} 60 | 61 | {help} 62 | {error} 63 | 64 | ); 65 | } 66 | 67 | module.exports = select; 68 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/list.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var { View, Text, TouchableHighlight } = require("react-native"); 3 | 4 | function renderRowWithoutButtons(item) { 5 | return {item.input}; 6 | } 7 | 8 | function renderRowButton(button, stylesheet, style) { 9 | return ( 10 | 15 | {button.label} 16 | 17 | ); 18 | } 19 | 20 | function renderButtonGroup(buttons, stylesheet) { 21 | return ( 22 | 23 | {buttons.map(button => 24 | renderRowButton(button, stylesheet, { width: 50 }) 25 | )} 26 | 27 | ); 28 | } 29 | 30 | function renderRow(item, stylesheet) { 31 | return ( 32 | 33 | {item.input} 34 | 35 | {renderButtonGroup(item.buttons, stylesheet)} 36 | 37 | 38 | ); 39 | } 40 | 41 | function list(locals) { 42 | if (locals.hidden) { 43 | return null; 44 | } 45 | 46 | var stylesheet = locals.stylesheet; 47 | var fieldsetStyle = stylesheet.fieldset; 48 | var controlLabelStyle = stylesheet.controlLabel.normal; 49 | 50 | if (locals.hasError) { 51 | controlLabelStyle = stylesheet.controlLabel.error; 52 | } 53 | 54 | var label = locals.label ? ( 55 | {locals.label} 56 | ) : null; 57 | var error = 58 | locals.hasError && locals.error ? ( 59 | 60 | {locals.error} 61 | 62 | ) : null; 63 | 64 | var rows = locals.items.map(function(item) { 65 | return item.buttons.length === 0 66 | ? renderRowWithoutButtons(item) 67 | : renderRow(item, stylesheet); 68 | }); 69 | 70 | var addButton = locals.add ? renderRowButton(locals.add, stylesheet) : null; 71 | 72 | return ( 73 | 74 | {label} 75 | {error} 76 | {rows} 77 | {addButton} 78 | 79 | ); 80 | } 81 | 82 | module.exports = list; 83 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/textbox.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var { View, Text, TextInput } = require("react-native"); 3 | 4 | function textbox(locals) { 5 | if (locals.hidden) { 6 | return null; 7 | } 8 | 9 | var stylesheet = locals.stylesheet; 10 | var formGroupStyle = stylesheet.formGroup.normal; 11 | var controlLabelStyle = stylesheet.controlLabel.normal; 12 | var textboxStyle = stylesheet.textbox.normal; 13 | var textboxViewStyle = stylesheet.textboxView.normal; 14 | var helpBlockStyle = stylesheet.helpBlock.normal; 15 | var errorBlockStyle = stylesheet.errorBlock; 16 | 17 | if (locals.hasError) { 18 | formGroupStyle = stylesheet.formGroup.error; 19 | controlLabelStyle = stylesheet.controlLabel.error; 20 | textboxStyle = stylesheet.textbox.error; 21 | textboxViewStyle = stylesheet.textboxView.error; 22 | helpBlockStyle = stylesheet.helpBlock.error; 23 | } 24 | 25 | if (locals.editable === false) { 26 | textboxStyle = stylesheet.textbox.notEditable; 27 | textboxViewStyle = stylesheet.textboxView.notEditable; 28 | } 29 | 30 | var label = locals.label ? ( 31 | {locals.label} 32 | ) : null; 33 | var help = locals.help ? ( 34 | {locals.help} 35 | ) : null; 36 | var error = 37 | locals.hasError && locals.error ? ( 38 | 39 | {locals.error} 40 | 41 | ) : null; 42 | 43 | return ( 44 | 45 | {label} 46 | 47 | locals.onChange(value)} 79 | onChange={locals.onChangeNative} 80 | placeholder={locals.placeholder} 81 | style={textboxStyle} 82 | value={locals.value} 83 | /> 84 | 85 | {help} 86 | {error} 87 | 88 | ); 89 | } 90 | 91 | module.exports = textbox; 92 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/datepicker.ios.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Text, 5 | View, 6 | Animated, 7 | DatePickerIOS, 8 | TouchableOpacity 9 | } from "react-native"; 10 | 11 | const UIPICKER_HEIGHT = 216; 12 | 13 | class CollapsibleDatePickerIOS extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this._onDateChange = this.onDateChange.bind(this); 17 | this._onPress = this.onPress.bind(this); 18 | this.state = { 19 | isCollapsed: true, 20 | height: new Animated.Value(0) 21 | }; 22 | } 23 | 24 | onDateChange(value) { 25 | this.props.locals.onChange(value); 26 | } 27 | 28 | onPress() { 29 | const locals = this.props.locals; 30 | let animation = Animated.timing; 31 | let animationConfig = { 32 | duration: 200 33 | }; 34 | if (locals.config) { 35 | if (locals.config.animation) { 36 | animation = locals.config.animation; 37 | } 38 | if (locals.config.animationConfig) { 39 | animationConfig = locals.config.animationConfig; 40 | } 41 | } 42 | animation( 43 | this.state.height, 44 | Object.assign( 45 | { 46 | toValue: this.state.isCollapsed ? UIPICKER_HEIGHT : 0 47 | }, 48 | animationConfig 49 | ) 50 | ).start(); 51 | this.setState({ isCollapsed: !this.state.isCollapsed }); 52 | if (typeof locals.onPress === "function") { 53 | locals.onPress(); 54 | } 55 | } 56 | 57 | render() { 58 | const locals = this.props.locals; 59 | const stylesheet = locals.stylesheet; 60 | let touchableStyle = stylesheet.dateTouchable.normal; 61 | let datepickerStyle = stylesheet.datepicker.normal; 62 | let dateValueStyle = stylesheet.dateValue.normal; 63 | if (locals.hasError) { 64 | touchableStyle = stylesheet.dateTouchable.error; 65 | datepickerStyle = stylesheet.datepicker.error; 66 | dateValueStyle = stylesheet.dateValue.error; 67 | } 68 | 69 | if (locals.disabled) { 70 | touchableStyle = stylesheet.dateTouchable.notEditable; 71 | } 72 | 73 | let formattedValue = locals.value ? String(locals.value) : ""; 74 | if (locals.config) { 75 | if (locals.config.format && formattedValue) { 76 | formattedValue = locals.config.format(locals.value); 77 | } else if (!formattedValue) { 78 | formattedValue = locals.config.defaultValueText 79 | ? locals.config.defaultValueText 80 | : "Tap here to select a date"; 81 | } 82 | } 83 | const height = this.state.isCollapsed ? 0 : UIPICKER_HEIGHT; 84 | return ( 85 | 86 | 91 | {formattedValue} 92 | 93 | 96 | 109 | 110 | 111 | ); 112 | } 113 | } 114 | 115 | CollapsibleDatePickerIOS.propTypes = { 116 | locals: PropTypes.object.isRequired 117 | }; 118 | 119 | function datepicker(locals) { 120 | if (locals.hidden) { 121 | return null; 122 | } 123 | 124 | const stylesheet = locals.stylesheet; 125 | let formGroupStyle = stylesheet.formGroup.normal; 126 | let controlLabelStyle = stylesheet.controlLabel.normal; 127 | let helpBlockStyle = stylesheet.helpBlock.normal; 128 | const errorBlockStyle = stylesheet.errorBlock; 129 | 130 | if (locals.hasError) { 131 | formGroupStyle = stylesheet.formGroup.error; 132 | controlLabelStyle = stylesheet.controlLabel.error; 133 | helpBlockStyle = stylesheet.helpBlock.error; 134 | } 135 | 136 | const label = locals.label ? ( 137 | {locals.label} 138 | ) : null; 139 | const help = locals.help ? ( 140 | {locals.help} 141 | ) : null; 142 | const error = 143 | locals.hasError && locals.error ? ( 144 | 145 | {locals.error} 146 | 147 | ) : null; 148 | 149 | return ( 150 | 151 | {label} 152 | 153 | {help} 154 | {error} 155 | 156 | ); 157 | } 158 | 159 | module.exports = datepicker; 160 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | import t, { mixin } from "tcomb-validation"; 2 | 3 | export function getOptionsOfEnum(type) { 4 | const enums = type.meta.map; 5 | return Object.keys(enums).map(value => { 6 | return { 7 | value, 8 | text: enums[value] 9 | }; 10 | }); 11 | } 12 | 13 | export function getTypeInfo(type) { 14 | let innerType = type; 15 | let isMaybe = false; 16 | let isSubtype = false; 17 | let kind; 18 | let innerGetValidationErrorMessage; 19 | 20 | while (innerType) { 21 | kind = innerType.meta.kind; 22 | if (t.Function.is(innerType.getValidationErrorMessage)) { 23 | innerGetValidationErrorMessage = innerType.getValidationErrorMessage; 24 | } 25 | if (kind === "maybe") { 26 | isMaybe = true; 27 | innerType = innerType.meta.type; 28 | continue; 29 | } 30 | if (kind === "subtype") { 31 | isSubtype = true; 32 | innerType = innerType.meta.type; 33 | continue; 34 | } 35 | break; 36 | } 37 | 38 | const getValidationErrorMessage = innerGetValidationErrorMessage 39 | ? (value, path, context) => { 40 | const result = t.validate(value, type, { path, context }); 41 | if (!result.isValid()) { 42 | for (let i = 0, len = result.errors.length; i < len; i++) { 43 | if ( 44 | t.Function.is(result.errors[i].expected.getValidationErrorMessage) 45 | ) { 46 | return result.errors[i].message; 47 | } 48 | } 49 | return innerGetValidationErrorMessage(value, path, context); 50 | } 51 | } 52 | : undefined; 53 | 54 | return { 55 | type, 56 | isMaybe, 57 | isSubtype, 58 | innerType, 59 | getValidationErrorMessage 60 | }; 61 | } 62 | 63 | // thanks to https://github.com/epeli/underscore.string 64 | 65 | function underscored(s) { 66 | return s 67 | .trim() 68 | .replace(/([a-z\d])([A-Z]+)/g, "$1_$2") 69 | .replace(/[-\s]+/g, "_") 70 | .toLowerCase(); 71 | } 72 | 73 | function capitalize(s) { 74 | return s.charAt(0).toUpperCase() + s.slice(1); 75 | } 76 | 77 | export function humanize(s) { 78 | return capitalize( 79 | underscored(s) 80 | .replace(/_id$/, "") 81 | .replace(/_/g, " ") 82 | ); 83 | } 84 | 85 | export function merge(a, b) { 86 | return mixin(mixin({}, a), b, true); 87 | } 88 | 89 | export function move(arr, fromIndex, toIndex) { 90 | const element = arr.splice(fromIndex, 1)[0]; 91 | arr.splice(toIndex, 0, element); 92 | return arr; 93 | } 94 | 95 | export class UIDGenerator { 96 | constructor(seed) { 97 | this.seed = "tfid-" + seed + "-"; 98 | this.counter = 0; 99 | } 100 | 101 | next() { 102 | return this.seed + this.counter++; // eslint-disable-line space-unary-ops 103 | } 104 | } 105 | 106 | function containsUnion(type) { 107 | switch (type.meta.kind) { 108 | case "union": 109 | return true; 110 | case "maybe": 111 | case "subtype": 112 | return containsUnion(type.meta.type); 113 | default: 114 | return false; 115 | } 116 | } 117 | 118 | function getUnionConcreteType(type, value) { 119 | const kind = type.meta.kind; 120 | if (kind === "union") { 121 | const concreteType = type.dispatch(value); 122 | if (process.env.NODE_ENV !== "production") { 123 | t.assert( 124 | t.isType(concreteType), 125 | () => 126 | "Invalid value " + 127 | t.assert.stringify(value) + 128 | " supplied to " + 129 | t.getTypeName(type) + 130 | " (no constructor returned by dispatch)" 131 | ); 132 | } 133 | return concreteType; 134 | } else if (kind === "maybe") { 135 | return t.maybe(getUnionConcreteType(type.meta.type, value), type.meta.name); 136 | } else if (kind === "subtype") { 137 | return t.subtype( 138 | getUnionConcreteType(type.meta.type, value), 139 | type.meta.predicate, 140 | type.meta.name 141 | ); 142 | } 143 | } 144 | 145 | export function getTypeFromUnion(type, value) { 146 | if (containsUnion(type)) { 147 | return getUnionConcreteType(type, value); 148 | } 149 | return type; 150 | } 151 | 152 | function getUnion(type) { 153 | if (type.meta.kind === "union") { 154 | return type; 155 | } 156 | return getUnion(type.meta.type); 157 | } 158 | 159 | function findIndex(arr, element) { 160 | for (let i = 0, len = arr.length; i < len; i++) { 161 | if (arr[i] === element) { 162 | return i; 163 | } 164 | } 165 | return -1; 166 | } 167 | 168 | export function getComponentOptions(options, defaultOptions, value, type) { 169 | if (t.Nil.is(options)) { 170 | return defaultOptions; 171 | } 172 | if (t.Function.is(options)) { 173 | return options(value); 174 | } 175 | if (t.Array.is(options) && containsUnion(type)) { 176 | const union = getUnion(type); 177 | const concreteType = union.dispatch(value); 178 | const index = findIndex(union.meta.types, concreteType); 179 | // recurse 180 | return getComponentOptions( 181 | options[index], 182 | defaultOptions, 183 | value, 184 | concreteType 185 | ); 186 | } 187 | return options; 188 | } 189 | -------------------------------------------------------------------------------- /docs/STYLESHEETS.md: -------------------------------------------------------------------------------- 1 | # Stylesheets 2 | 3 | There are several levels of customization in `tcomb-form-native`: 4 | 5 | - stylesheets 6 | - templates 7 | - factories 8 | 9 | Let's focus on the first one: stylesheets. 10 | 11 | ## Basics and tech details 12 | 13 | Let's define a simple type useful in the following examples: 14 | 15 | ```js 16 | const Type = t.struct({ 17 | name: t.String 18 | }); 19 | ``` 20 | 21 | By default `tcomb-form-native` will display a textbox styled with a boostrap-like look and feel. The default stylesheet is defined [here](https://github.com/gcanti/tcomb-form-native/blob/master/lib/stylesheets/bootstrap.js). 22 | 23 | This is the normal look and feel of a textbox (border: gray) 24 | 25 | ![](images/stylesheets/1.png) 26 | 27 | and this is the look and feel when an error occurs (border: red) 28 | 29 | ![](images/stylesheets/2.png) 30 | 31 | The style management is coded in the `lib/stylesheets/bootstrap` module, specifically by the following lines: 32 | 33 | ```js 34 | textbox: { 35 | 36 | // the style applied wihtout errors 37 | normal: { 38 | color: '#000000', 39 | fontSize: 17, 40 | height: 36, 41 | padding: 7, 42 | borderRadius: 4, 43 | borderColor: '#cccccc', // <= relevant style here 44 | borderWidth: 1, 45 | marginBottom: 5 46 | }, 47 | 48 | // the style applied when a validation error occours 49 | error: { 50 | color: '#000000', 51 | fontSize: 17, 52 | height: 36, 53 | padding: 7, 54 | borderRadius: 4, 55 | borderColor: '#a94442', // <= relevant style here 56 | borderWidth: 1, 57 | marginBottom: 5 58 | } 59 | 60 | } 61 | ``` 62 | 63 | Depending on the state of the textbox, `tcomb-form-native` passes in the proper style to the `` RN component (code [here](https://github.com/gcanti/tcomb-form-native/blob/master/lib/templates/bootstrap/textbox.js)). 64 | 65 | You can override the default stylesheet both locally and globally. 66 | 67 | ## Overriding the style locally 68 | 69 | Say you want the text entered in a texbox being green: 70 | 71 | ```js 72 | var t = require('tcomb-form-native'); 73 | var _ = require('lodash'); 74 | 75 | // clone the default stylesheet 76 | const stylesheet = _.cloneDeep(t.form.Form.stylesheet); 77 | 78 | // overriding the text color 79 | stylesheet.textbox.normal.color = '#00FF00'; 80 | 81 | const options = { 82 | fields: { 83 | name: { 84 | stylesheet: stylesheet // overriding the style of the textbox 85 | } 86 | } 87 | }; 88 | 89 | ... 90 | 91 | // other forms in you app won't be affected 92 | 93 | ``` 94 | 95 | **Output** 96 | 97 | ![](images/stylesheets/3.png) 98 | 99 | **Note**. This is the list of styles that you can override: 100 | 101 | - textbox 102 | - normal 103 | - error 104 | - notEditable 105 | - checkbox 106 | - normal 107 | - error 108 | - select 109 | - normal 110 | - error 111 | - datepicker 112 | - normal 113 | - error 114 | - formGroup 115 | - controlLabel 116 | - helpBlock 117 | - errorBlock 118 | - textboxView 119 | 120 | ## Overriding the style globally 121 | 122 | Just omit the `deepclone` call, the style will be applied to all textboxes 123 | 124 | ```js 125 | var t = require('tcomb-form-native'); 126 | 127 | // overriding the text color for every textbox in every form of your app 128 | t.form.Form.stylesheet.textbox.normal.color = '#00FF00'; 129 | ``` 130 | 131 | ## Examples 132 | 133 | ### Horizontal forms 134 | 135 | Let's add a `surname` field: 136 | 137 | ```js 138 | const Type = t.struct({ 139 | name: t.String, 140 | surname: t.String 141 | }); 142 | ``` 143 | 144 | The default layout is vertical: 145 | 146 | ![](images/stylesheets/4.png) 147 | 148 | I'll use flexbox in order to display the textboxes horizontally: 149 | 150 | ```js 151 | var _ = require('lodash'); 152 | 153 | const stylesheet = _.cloneDeep(t.form.Form.stylesheet); 154 | 155 | stylesheet.fieldset = { 156 | flexDirection: 'row' 157 | }; 158 | stylesheet.formGroup.normal.flex = 1; 159 | stylesheet.formGroup.error.flex = 1; 160 | 161 | const options = { 162 | stylesheet: stylesheet 163 | }; 164 | ``` 165 | 166 | **Output** 167 | 168 | ![](images/stylesheets/5.png) 169 | 170 | ### Label on the left side 171 | 172 | ```js 173 | var _ = require('lodash'); 174 | 175 | const stylesheet = _.cloneDeep(t.form.Form.stylesheet); 176 | 177 | stylesheet.formGroup.normal.flexDirection = 'row'; 178 | stylesheet.formGroup.error.flexDirection = 'row'; 179 | stylesheet.textboxView.normal.flex = 1; 180 | stylesheet.textboxView.error.flex = 1; 181 | 182 | const options = { 183 | stylesheet: stylesheet 184 | }; 185 | ``` 186 | 187 | **Output** 188 | 189 | ![](images/stylesheets/6.png) 190 | 191 | 192 | ### Material Design Style Underlines 193 | 194 | ```js 195 | var _ = require('lodash'); 196 | 197 | const stylesheet = _.cloneDeep(t.form.Form.stylesheet); 198 | 199 | stylesheet.textbox.normal.borderWidth = 0; 200 | stylesheet.textbox.error.borderWidth = 0; 201 | stylesheet.textbox.normal.marginBottom = 0; 202 | stylesheet.textbox.error.marginBottom = 0; 203 | 204 | stylesheet.textboxView.normal.borderWidth = 0; 205 | stylesheet.textboxView.error.borderWidth = 0; 206 | stylesheet.textboxView.normal.borderRadius = 0; 207 | stylesheet.textboxView.error.borderRadius = 0; 208 | stylesheet.textboxView.normal.borderBottomWidth = 1; 209 | stylesheet.textboxView.error.borderBottomWidth = 1; 210 | stylesheet.textbox.normal.marginBottom = 5; 211 | stylesheet.textbox.error.marginBottom = 5; 212 | 213 | const options = { 214 | stylesheet: stylesheet 215 | }; 216 | ``` 217 | 218 | **Output** 219 | 220 | ![](images/stylesheets/7.png) 221 | 222 | -------------------------------------------------------------------------------- /lib/stylesheets/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | a bootstrap like style 4 | 5 | */ 6 | "use strict"; 7 | 8 | import { Platform } from "react-native"; 9 | 10 | var LABEL_COLOR = "#000000"; 11 | var INPUT_COLOR = "#000000"; 12 | var ERROR_COLOR = "#a94442"; 13 | var HELP_COLOR = "#999999"; 14 | var BORDER_COLOR = "#cccccc"; 15 | var DISABLED_COLOR = "#777777"; 16 | var DISABLED_BACKGROUND_COLOR = "#eeeeee"; 17 | var FONT_SIZE = 17; 18 | var FONT_WEIGHT = "500"; 19 | 20 | var stylesheet = Object.freeze({ 21 | fieldset: {}, 22 | // the style applied to the container of all inputs 23 | formGroup: { 24 | normal: { 25 | marginBottom: 10 26 | }, 27 | error: { 28 | marginBottom: 10 29 | } 30 | }, 31 | controlLabel: { 32 | normal: { 33 | color: LABEL_COLOR, 34 | fontSize: FONT_SIZE, 35 | marginBottom: 7, 36 | fontWeight: FONT_WEIGHT 37 | }, 38 | // the style applied when a validation error occours 39 | error: { 40 | color: ERROR_COLOR, 41 | fontSize: FONT_SIZE, 42 | marginBottom: 7, 43 | fontWeight: FONT_WEIGHT 44 | } 45 | }, 46 | helpBlock: { 47 | normal: { 48 | color: HELP_COLOR, 49 | fontSize: FONT_SIZE, 50 | marginBottom: 2 51 | }, 52 | // the style applied when a validation error occours 53 | error: { 54 | color: HELP_COLOR, 55 | fontSize: FONT_SIZE, 56 | marginBottom: 2 57 | } 58 | }, 59 | errorBlock: { 60 | fontSize: FONT_SIZE, 61 | marginBottom: 2, 62 | color: ERROR_COLOR 63 | }, 64 | textboxView: { 65 | normal: {}, 66 | error: {}, 67 | notEditable: {} 68 | }, 69 | textbox: { 70 | normal: { 71 | color: INPUT_COLOR, 72 | fontSize: FONT_SIZE, 73 | height: 36, 74 | paddingVertical: Platform.OS === "ios" ? 7 : 0, 75 | paddingHorizontal: 7, 76 | borderRadius: 4, 77 | borderColor: BORDER_COLOR, 78 | borderWidth: 1, 79 | marginBottom: 5 80 | }, 81 | // the style applied when a validation error occours 82 | error: { 83 | color: INPUT_COLOR, 84 | fontSize: FONT_SIZE, 85 | height: 36, 86 | paddingVertical: Platform.OS === "ios" ? 7 : 0, 87 | paddingHorizontal: 7, 88 | borderRadius: 4, 89 | borderColor: ERROR_COLOR, 90 | borderWidth: 1, 91 | marginBottom: 5 92 | }, 93 | // the style applied when the textbox is not editable 94 | notEditable: { 95 | fontSize: FONT_SIZE, 96 | height: 36, 97 | paddingVertical: Platform.OS === "ios" ? 7 : 0, 98 | paddingHorizontal: 7, 99 | borderRadius: 4, 100 | borderColor: BORDER_COLOR, 101 | borderWidth: 1, 102 | marginBottom: 5, 103 | color: DISABLED_COLOR, 104 | backgroundColor: DISABLED_BACKGROUND_COLOR 105 | } 106 | }, 107 | checkbox: { 108 | normal: { 109 | marginBottom: 4 110 | }, 111 | // the style applied when a validation error occours 112 | error: { 113 | marginBottom: 4 114 | } 115 | }, 116 | pickerContainer: { 117 | normal: { 118 | marginBottom: 4, 119 | borderRadius: 4, 120 | borderColor: BORDER_COLOR, 121 | borderWidth: 1 122 | }, 123 | error: { 124 | marginBottom: 4, 125 | borderRadius: 4, 126 | borderColor: ERROR_COLOR, 127 | borderWidth: 1 128 | }, 129 | open: { 130 | // Alter styles when select container is open 131 | } 132 | }, 133 | select: { 134 | normal: Platform.select({ 135 | android: { 136 | paddingLeft: 7, 137 | color: INPUT_COLOR 138 | }, 139 | ios: {} 140 | }), 141 | // the style applied when a validation error occours 142 | error: Platform.select({ 143 | android: { 144 | paddingLeft: 7, 145 | color: ERROR_COLOR 146 | }, 147 | ios: {} 148 | }) 149 | }, 150 | pickerTouchable: { 151 | normal: { 152 | height: 44, 153 | flexDirection: "row", 154 | alignItems: "center" 155 | }, 156 | error: { 157 | height: 44, 158 | flexDirection: "row", 159 | alignItems: "center" 160 | }, 161 | active: { 162 | borderBottomWidth: 1, 163 | borderColor: BORDER_COLOR 164 | }, 165 | notEditable: { 166 | height: 44, 167 | flexDirection: "row", 168 | alignItems: "center", 169 | backgroundColor: DISABLED_BACKGROUND_COLOR 170 | } 171 | }, 172 | pickerValue: { 173 | normal: { 174 | fontSize: FONT_SIZE, 175 | paddingLeft: 7 176 | }, 177 | error: { 178 | fontSize: FONT_SIZE, 179 | paddingLeft: 7 180 | } 181 | }, 182 | datepicker: { 183 | normal: { 184 | marginBottom: 4 185 | }, 186 | // the style applied when a validation error occours 187 | error: { 188 | marginBottom: 4 189 | } 190 | }, 191 | dateTouchable: { 192 | normal: {}, 193 | error: {}, 194 | notEditable: { 195 | backgroundColor: DISABLED_BACKGROUND_COLOR 196 | } 197 | }, 198 | dateValue: { 199 | normal: { 200 | color: INPUT_COLOR, 201 | fontSize: FONT_SIZE, 202 | padding: 7, 203 | marginBottom: 5 204 | }, 205 | error: { 206 | color: ERROR_COLOR, 207 | fontSize: FONT_SIZE, 208 | padding: 7, 209 | marginBottom: 5 210 | } 211 | }, 212 | buttonText: { 213 | fontSize: 18, 214 | color: "white", 215 | alignSelf: "center" 216 | }, 217 | button: { 218 | height: 36, 219 | backgroundColor: "#48BBEC", 220 | borderColor: "#48BBEC", 221 | borderWidth: 1, 222 | borderRadius: 8, 223 | marginBottom: 10, 224 | alignSelf: "stretch", 225 | justifyContent: "center" 226 | } 227 | }); 228 | 229 | module.exports = stylesheet; 230 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/select.ios.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Animated, View, TouchableOpacity, Text, Picker } from "react-native"; 4 | 5 | const UIPICKER_HEIGHT = 216; 6 | 7 | class CollapsiblePickerIOS extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | const isCollapsed = 11 | typeof this.props.locals.isCollapsed === typeof true 12 | ? this.props.locals.isCollapsed 13 | : true; 14 | 15 | this.state = { 16 | isCollapsed, 17 | height: new Animated.Value(isCollapsed ? 0 : UIPICKER_HEIGHT) 18 | }; 19 | this.animatePicker = this.animatePicker.bind(this); 20 | this.togglePicker = this.togglePicker.bind(this); 21 | } 22 | 23 | componentWillReceiveProps(props) { 24 | const isCollapsed = 25 | typeof props.locals.isCollapsed === typeof true 26 | ? props.locals.isCollapsed 27 | : this.props.locals.isCollapsed; 28 | if (isCollapsed !== this.state.isCollapsed) { 29 | this.animatePicker(isCollapsed); 30 | this.setState({ isCollapsed }); 31 | if (typeof props.locals.onCollapseChange === "function") { 32 | props.locals.onCollapseChange(isCollapsed); 33 | } 34 | } 35 | } 36 | 37 | animatePicker(isCollapsed) { 38 | const locals = this.props.locals; 39 | let animation = Animated.timing; 40 | let animationConfig = { 41 | duration: 200 42 | }; 43 | if (locals.config) { 44 | if (locals.config.animation) { 45 | animation = locals.config.animation; 46 | } 47 | if (locals.config.animationConfig) { 48 | animationConfig = locals.config.animationConfig; 49 | } 50 | } 51 | 52 | animation( 53 | this.state.height, 54 | Object.assign( 55 | { 56 | toValue: isCollapsed ? 0 : UIPICKER_HEIGHT 57 | }, 58 | animationConfig 59 | ) 60 | ).start(); 61 | } 62 | 63 | togglePicker() { 64 | this.setState({ isCollapsed: !this.state.isCollapsed }, () => { 65 | this.animatePicker(this.state.isCollapsed); 66 | if (typeof this.props.locals.onCollapseChange === "function") { 67 | this.props.locals.onCollapseChange(this.state.isCollapsed); 68 | } 69 | }); 70 | } 71 | 72 | render() { 73 | const locals = this.props.locals; 74 | const { stylesheet } = locals; 75 | let pickerContainer = stylesheet.pickerContainer.normal; 76 | let pickerContainerOpen = stylesheet.pickerContainer.open; 77 | let selectStyle = stylesheet.select.normal; 78 | let touchableStyle = stylesheet.pickerTouchable.normal; 79 | let touchableStyleActive = stylesheet.pickerTouchable.active; 80 | let pickerValue = stylesheet.pickerValue.normal; 81 | if (locals.hasError) { 82 | pickerContainer = stylesheet.pickerContainer.error; 83 | selectStyle = stylesheet.select.error; 84 | touchableStyle = stylesheet.pickerTouchable.error; 85 | pickerValue = stylesheet.pickerValue.error; 86 | } 87 | 88 | if (locals.disabled) { 89 | touchableStyle = stylesheet.pickerTouchable.notEditable; 90 | } 91 | 92 | const options = locals.options.map(({ value, text }) => ( 93 | 94 | )); 95 | const selectedOption = locals.options.find( 96 | option => option.value === locals.value 97 | ); 98 | 99 | const height = this.state.isCollapsed ? 0 : UIPICKER_HEIGHT; 100 | return ( 101 | 107 | 115 | {selectedOption.text} 116 | 117 | 120 | 132 | {options} 133 | 134 | 135 | 136 | ); 137 | } 138 | } 139 | 140 | CollapsiblePickerIOS.propTypes = { 141 | locals: PropTypes.object.isRequired 142 | }; 143 | 144 | function select(locals) { 145 | if (locals.hidden) { 146 | return null; 147 | } 148 | 149 | const stylesheet = locals.stylesheet; 150 | let formGroupStyle = stylesheet.formGroup.normal; 151 | let controlLabelStyle = stylesheet.controlLabel.normal; 152 | let selectStyle = stylesheet.select.normal; 153 | let helpBlockStyle = stylesheet.helpBlock.normal; 154 | let errorBlockStyle = stylesheet.errorBlock; 155 | 156 | if (locals.hasError) { 157 | formGroupStyle = stylesheet.formGroup.error; 158 | controlLabelStyle = stylesheet.controlLabel.error; 159 | selectStyle = stylesheet.select.error; 160 | helpBlockStyle = stylesheet.helpBlock.error; 161 | } 162 | 163 | const label = locals.label ? ( 164 | {locals.label} 165 | ) : null; 166 | const help = locals.help ? ( 167 | {locals.help} 168 | ) : null; 169 | const error = 170 | locals.hasError && locals.error ? ( 171 | 172 | {locals.error} 173 | 174 | ) : null; 175 | 176 | var options = locals.options.map(({ value, text }) => ( 177 | 178 | )); 179 | 180 | return ( 181 | 182 | {label} 183 | 184 | {help} 185 | {error} 186 | 187 | ); 188 | } 189 | 190 | module.exports = select; 191 | -------------------------------------------------------------------------------- /lib/templates/bootstrap/datepicker.android.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | Text, 5 | DatePickerAndroid, 6 | TimePickerAndroid, 7 | TouchableNativeFeedback 8 | } from "react-native"; 9 | 10 | function datepicker(locals) { 11 | if (locals.hidden) { 12 | return null; 13 | } 14 | 15 | var stylesheet = locals.stylesheet; 16 | var formGroupStyle = stylesheet.formGroup.normal; 17 | var controlLabelStyle = stylesheet.controlLabel.normal; 18 | var datepickerStyle = stylesheet.datepicker.normal; 19 | var helpBlockStyle = stylesheet.helpBlock.normal; 20 | var errorBlockStyle = stylesheet.errorBlock; 21 | var dateValueStyle = stylesheet.dateValue.normal; 22 | 23 | if (locals.hasError) { 24 | formGroupStyle = stylesheet.formGroup.error; 25 | controlLabelStyle = stylesheet.controlLabel.error; 26 | datepickerStyle = stylesheet.datepicker.error; 27 | helpBlockStyle = stylesheet.helpBlock.error; 28 | dateValueStyle = stylesheet.dateValue.error; 29 | } 30 | 31 | // Setup the picker mode 32 | var datePickerMode = locals.mode; 33 | if ( 34 | datePickerMode !== "date" && 35 | datePickerMode !== "time" && 36 | datePickerMode !== "datetime" 37 | ) { 38 | throw new Error(`Unrecognized date picker format ${datePickerMode}`); 39 | } 40 | 41 | /** 42 | * Check config locals for Android datepicker. 43 | * `locals.config.background: `TouchableNativeFeedback` background prop 44 | * `locals.config.format`: Date format function 45 | * `locals.config.dialogMode`: 'calendar', 'spinner', 'default' 46 | * `locals.config.dateFormat`: Date only format 47 | * `locals.config.timeFormat`: Time only format 48 | */ 49 | var formattedValue = locals.value ? String(locals.value) : ""; 50 | var background = TouchableNativeFeedback.SelectableBackground(); // eslint-disable-line new-cap 51 | var dialogMode = "default"; 52 | var formattedDateValue = locals.value ? locals.value.toDateString() : ""; 53 | var formattedTimeValue = locals.value ? locals.value.toTimeString() : ""; 54 | if (locals.config) { 55 | if (locals.config.format && formattedValue) { 56 | formattedValue = locals.config.format(locals.value); 57 | } else if (!formattedValue) { 58 | formattedValue = locals.config.defaultValueText 59 | ? locals.config.defaultValueText 60 | : "Tap here to select a date"; 61 | } 62 | if (locals.config.background) { 63 | background = locals.config.background; 64 | } 65 | if (locals.config.dialogMode) { 66 | dialogMode = locals.config.dialogMode; 67 | } 68 | if (locals.config.dateFormat && formattedDateValue) { 69 | formattedDateValue = locals.config.dateFormat(locals.value); 70 | } else if (!formattedDateValue) { 71 | formattedDateValue = locals.config.defaultValueText 72 | ? locals.config.defaultValueText 73 | : "Tap here to select a date"; 74 | } 75 | if (locals.config.timeFormat && formattedTimeValue) { 76 | formattedTimeValue = locals.config.timeFormat(locals.value); 77 | } else if (!formattedTimeValue) { 78 | formattedTimeValue = locals.config.defaultValueText 79 | ? locals.config.defaultValueText 80 | : "Tap here to select a time"; 81 | } 82 | } 83 | 84 | var label = locals.label ? ( 85 | {locals.label} 86 | ) : null; 87 | var help = locals.help ? ( 88 | {locals.help} 89 | ) : null; 90 | var error = 91 | locals.hasError && locals.error ? ( 92 | 93 | {locals.error} 94 | 95 | ) : null; 96 | var value = formattedValue ? ( 97 | {formattedValue} 98 | ) : null; 99 | 100 | return ( 101 | 102 | {datePickerMode === "datetime" ? ( 103 | 104 | {label} 105 | 133 | 134 | {formattedDateValue} 135 | 136 | 137 | 166 | 167 | {formattedTimeValue} 168 | 169 | 170 | 171 | ) : ( 172 | 220 | 221 | {label} 222 | {value} 223 | 224 | 225 | )} 226 | {help} 227 | {error} 228 | 229 | ); 230 | } 231 | 232 | module.exports = datepicker; 233 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > - [New Feature] 5 | > - [Bug Fix] 6 | > - [Breaking Change] 7 | > - [Documentation] 8 | > - [Internal] 9 | > - [Polish] 10 | 11 | **Note**: Gaps between patch versions are faulty/broken releases. 12 | 13 | ## 0.6.11 14 | 15 | - **New Feature** 16 | - add support for datetime in Android, closes #283 (@alvaromb) 17 | - **Polish** 18 | - lib uses prettier formatting (@alvaromb) 19 | 20 | ## 0.6.10 21 | 22 | - **New Feature** 23 | - add pureValidate method on Form (@papuaaaaaaa) 24 | - add option to change dialog mode for DatePickerAndroid (@javiercr) 25 | - add onPress handler for DatePicker (@koenpunt) 26 | - **Bug fix** 27 | - remove checkbox label when auto === 'none' (@OzoTek) 28 | - **Polish** 29 | - fix broken link in README (@Hitabis) 30 | - add version support table, closes #248 (@alvaromb) 31 | - updated React to 15.6.1 and added PropTypes Dependency (@garylesueur) 32 | 33 | ## 0.6.9 34 | 35 | - **Bug fix** 36 | - Android timepicker always open the current time (@francescjimenez) 37 | 38 | ## 0.6.8 39 | 40 | - **New Feature** 41 | - allow disabling datepicker by passing disabled prop to touchableopacity we can disable the datepicker (@koenpunt) 42 | - **Bug fix** 43 | - Add proper border color to select.ios when there is an error, fix #342 (@javiercr) 44 | 45 | ## 0.6.7 46 | 47 | - **Bug fix** 48 | - fix #301, PR https://github.com/gcanti/tcomb-form-native/pull/329 (@danilvalov) 49 | 50 | ## 0.6.6 51 | 52 | - **Polish** 53 | - Solves scroll view issues with text fields in Android, fix #301 (@alvaromb) 54 | 55 | ## v0.6.5 56 | 57 | - **New Feature** 58 | - Added `onContentSizeChange` as an option for TextBox (@nemo) 59 | 60 | ## v0.6.4 61 | 62 | - **New Feature** 63 | - Wrap TextInput in a View for More Customizable Styling, e.g. [Material Design Style Underlines](https://github.com/gcanti/tcomb-form-native/blob/master/docs/STYLESHEETS.md#material-design-style-underlines) (@kenleezle) 64 | 65 | ## v0.6.3 66 | 67 | - **New Feature** 68 | - Ability to style picker container (@dbonner1987) 69 | 70 | ## v0.6.2 71 | 72 | - **New Feature** 73 | - Make `underlineColorAndroid` transparent by default, fix #239 (@adamrainsby) 74 | 75 | ## v0.6.1 76 | 77 | - **New Feature** 78 | - Added support for minDate and maxDate in Android TimePickerAndroid (@alvaromb) 79 | 80 | ## v0.6.0 81 | 82 | - **Breaking Change** 83 | - Added collapsable iOS DatePicker (@alvaromb) 84 | - Added collapsable iOS Picker (@alvaromb) 85 | 86 | ## v0.5.3 87 | 88 | - **Bug Fix** 89 | - `_nativeContainerInfo` no longer exists in React v15.2.0, use `_hostContainerInfo` instead, fix #195 (@gcanti) 90 | 91 | ## v0.5.2 92 | 93 | - **New Feature** 94 | - add support for unions, fix #118 (@gcanti) 95 | - add support for lists, fix #80 (@gcanti) 96 | - **Bug Fix** 97 | - allow to set a default value in Android date picker template, fix #187 98 | 99 | ## v0.5.1 100 | 101 | - **New Feature** 102 | - Add onChange handler to TextInput, fix #168 (@gcanti) 103 | 104 | ## v0.5.0 105 | 106 | - **Breaking Change** 107 | - upgrade to `tcomb-validation` ^3.0.0 (@gcanti) 108 | - React API must be now required from `react` package (@jebschiefer) 109 | - **New Feature** 110 | - Updated support for TextInput props for RN>=0.25 (@alvaromb) 111 | 112 | ## v0.4.4 113 | 114 | - **Bug Fix** 115 | - Revert to export method from 869dd50 (https://github.com/gcanti/tcomb-form-native/pull/160) 116 | 117 | ## v0.4.3 118 | 119 | - **New Feature** 120 | - support hot reload, fix #132 (thanks @justim, @alvaromb) 121 | - add `hidden` option [docs](https://github.com/gcanti/tcomb-form-native#hidden-component) (thanks @miqmago) 122 | 123 | ## v0.4.2 124 | 125 | - **Bug Fix** 126 | - Textbox component displays extra characters after `getValue()`, fix #142 (thanks @alvaromb) 127 | 128 | ## v0.4.1 129 | 130 | - **New Feature** 131 | - Set `accessibilityLabel` and `accessibilityLiveRegion` attributes on form controls, fix #137 (thanks @ndarilek) 132 | 133 | ## v0.4.0 134 | 135 | - **Breaking Change** 136 | - required react-native version >= 0.20.0 137 | - **New Feature** 138 | - add support for Switch (Android), fix #60 (thanks @alvaromb) 139 | - Support for Android date and time pickers, fix #67 (thanks @alvaromb) 140 | - add support for webpack, fix #23 141 | - **Documentation** 142 | - How to clear form after submit (thanks @shashi-dokania) 143 | - Dynamic forms example: how to change a form based on selection 144 | - Stylesheet guide (docs/STYLESHEETS.md) 145 | - **Polish** 146 | - add travis CI 147 | - add ISSUE_TEMPLATE.md (new GitHub feature) 148 | 149 | ## v0.3.3 150 | 151 | - **Bug Fix** 152 | - v0.16.0 Warning: Form(...): React component classes must extend React.Component, fix #83 153 | 154 | ## v0.3.2 155 | 156 | - **Polish** 157 | - Map value to enum, fix #56 158 | 159 | ## v0.3.1 160 | 161 | - **New Feature** 162 | - default templates are now split in standalone files, so you can cherry pick which ones to load 163 | - ability to customize templates, stylesheets and i18n without loading the default ones (improved docs) 164 | - porting of tcomb-form `getValidationErrorMessage` feature 165 | 166 | ```js 167 | var Age = t.refinement(t.Number, function (n) { return n >= 18; }); 168 | 169 | // if you define a getValidationErrorMessage function, it will be called on validation errors 170 | Age.getValidationErrorMessage = function (value) { 171 | return 'bad age ' + value; 172 | }; 173 | 174 | var Schema = t.struct({ 175 | age: Age 176 | }); 177 | ``` 178 | 179 | - add support for nested forms, fix #43 180 | - add proper support for struct refinements 181 | - add support for struct label (bootstrap templates) 182 | 183 | **Example** 184 | 185 | ```js 186 | var Account = t.struct({ 187 | email: t.String, 188 | profile: t.struct({ 189 | name: t.String, 190 | surname: t.String 191 | }) 192 | }); 193 | 194 | var options = { 195 | label: Account, 196 | fields: { 197 | profile: { 198 | label: Profile 199 | } 200 | } 201 | }; 202 | ``` 203 | 204 | - add support for struct error (bootstrap templates) 205 | - **Experimental** 206 | - add support for maybe structs 207 | 208 | **Example** 209 | 210 | ```js 211 | var Account = t.struct({ 212 | email: t.String, 213 | profile: t.maybe(t.struct({ 214 | name: t.String, 215 | surname: t.String 216 | })) 217 | }); 218 | 219 | // user enters email: 'aaa', => result { email: 'aaa', profile: null } 220 | // user enters email: 'aaa', name: 'bbb' => validation error for surname 221 | // user enters email: 'aaa', name: 'bbb', surname: 'ccc' => result { email: 'aaa', profile: { name: 'bbb', surname: 'ccc' } } 222 | ``` 223 | 224 | ## v0.3.0 225 | 226 | - **Breaking Change** 227 | - Upgrade tcomb-validation to v2, fix #68 228 | 229 | Do not worry: the migration path should be seamless since the major version bump was caused by dropping the support for bower (i.e. types and combinators are the same). 230 | Just notice that the short type alias (`t.Str`, `t.Num`, ...) are deprecated in favour of the long ones (`t.String`, `t.Number`, ...) and the `subtype` combinator has now a more descriptive alias `refinement`. 231 | - **Bug Fix** 232 | - amend struct onChange, fix #70 233 | 234 | the previous code would lead to bugs regarding error messages when the 235 | type is a subtype of a struct, https://github.com/gcanti/tcomb-form/issues/235 236 | - **Internal** 237 | - move peer dependencies to dependencies 238 | 239 | ## v0.2.8 240 | 241 | - **New Feature** 242 | - Pass in config options to custom template through options, fix #63 243 | 244 | ## v0.2.7 245 | 246 | - **Internal** 247 | - support react-native versions greater than 0.9 248 | 249 | ## v0.2.6 250 | 251 | - **Internal** 252 | - pin react-native version to 0.9 253 | 254 | ## v0.2.5 255 | 256 | - **New Feature** 257 | + Add `required` field to i18n, fix #46 258 | - **Internal** 259 | + upgrade to react-native v0.9 260 | 261 | ## v0.2.4 262 | 263 | - **Bug Fix** 264 | + the default date for DatePicker is reflected in the UI but not in the `getValue()` API #42 265 | 266 | ## v0.2.3 267 | 268 | - **New Feature** 269 | + add auto option override for specific field #41 270 | 271 | ## v0.2.2 272 | 273 | - **Internal** 274 | + Recommend making peerDependencies more flexible #30 275 | 276 | ## v0.2.1 277 | 278 | - **Internal** 279 | + less padding to textboxes, #31 280 | 281 | ## v0.2.0 282 | 283 | - **New Feature** 284 | + getComponent API fix #19 285 | + get access to the native input contained in tcomb-form-native's component fix #24 286 | - **Breaking Change** 287 | + Inputs refactoring, this affects how to build custom inputs #12 288 | - **Internal** 289 | + Textbox is no more a controlled input #26 290 | + Add eslint 291 | 292 | ## v0.1.9 293 | 294 | - **Internal** 295 | + upgrade to react-native v0.5.0 fix #22 296 | 297 | ## v0.1.8 298 | 299 | - **Internal** 300 | - upgrade to react-native v0.4.3 301 | - upgrade to tcomb-validation v1.0.4 302 | 303 | ## v0.1.7 304 | 305 | - **Internal** 306 | - upgrade to react-native v0.4.0 307 | 308 | ## v0.1.6 309 | 310 | - **Internal** 311 | - upgrade to react-native v0.3.4 312 | - added tests 313 | - **New Feature** 314 | - add path field to contexts in order to provide correct error paths for validations, fix #5 315 | - added a path argument to onChange in order to know what field changed 316 | - added support for transformers (all components) 317 | - Password field type, fix #4 318 | - **Bug Fix** 319 | - Error with decimal mark in numeric field, fix #7 320 | -------------------------------------------------------------------------------- /lib/components.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import t from "tcomb-validation"; 3 | import { 4 | humanize, 5 | merge, 6 | getTypeInfo, 7 | getOptionsOfEnum, 8 | move, 9 | UIDGenerator, 10 | getTypeFromUnion, 11 | getComponentOptions 12 | } from "./util"; 13 | 14 | const SOURCE = "tcomb-form-native"; 15 | const nooptions = Object.freeze({}); 16 | const noop = function() {}; 17 | const noobj = Object.freeze({}); 18 | const noarr = Object.freeze([]); 19 | const Nil = t.Nil; 20 | 21 | function getFormComponent(type, options) { 22 | if (options.factory) { 23 | return options.factory; 24 | } 25 | if (type.getTcombFormFactory) { 26 | return type.getTcombFormFactory(options); 27 | } 28 | const name = t.getTypeName(type); 29 | switch (type.meta.kind) { 30 | case "irreducible": 31 | return type === t.Boolean 32 | ? Checkbox 33 | : type === t.Date 34 | ? DatePicker 35 | : Textbox; 36 | case "struct": 37 | return Struct; 38 | case "list": 39 | return List; 40 | case "enums": 41 | return Select; 42 | case "maybe": 43 | case "subtype": 44 | return getFormComponent(type.meta.type, options); 45 | default: 46 | t.fail(`[${SOURCE}] unsupported type ${name}`); 47 | } 48 | } 49 | 50 | function sortByText(a, b) { 51 | return a.text < b.text ? -1 : a.text > b.text ? 1 : 0; 52 | } 53 | 54 | function getComparator(order) { 55 | return { 56 | asc: sortByText, 57 | desc: (a, b) => -sortByText(a, b) 58 | }[order]; 59 | } 60 | 61 | class Component extends React.Component { 62 | constructor(props) { 63 | super(props); 64 | this.typeInfo = getTypeInfo(props.type); 65 | this.state = { 66 | hasError: false, 67 | value: this.getTransformer().format(props.value) 68 | }; 69 | } 70 | 71 | getTransformer() { 72 | return this.props.options.transformer || this.constructor.transformer; 73 | } 74 | 75 | shouldComponentUpdate(nextProps, nextState) { 76 | const should = 77 | nextState.value !== this.state.value || 78 | nextState.hasError !== this.state.hasError || 79 | nextProps.options !== this.props.options || 80 | nextProps.type !== this.props.type; 81 | return should; 82 | } 83 | 84 | componentWillReceiveProps(props) { 85 | if (props.type !== this.props.type) { 86 | this.typeInfo = getTypeInfo(props.type); 87 | } 88 | this.setState({ value: this.getTransformer().format(props.value) }); 89 | } 90 | 91 | onChange(value) { 92 | this.setState({ value }, () => 93 | this.props.onChange(value, this.props.ctx.path) 94 | ); 95 | } 96 | 97 | getValidationOptions() { 98 | return { 99 | path: this.props.ctx.path, 100 | context: t.mixin( 101 | t.mixin({}, this.props.context || this.props.ctx.context), 102 | { options: this.props.options } 103 | ) 104 | }; 105 | } 106 | 107 | getValue() { 108 | return this.getTransformer().parse(this.state.value); 109 | } 110 | 111 | isValueNully() { 112 | return Nil.is(this.getValue()); 113 | } 114 | 115 | removeErrors() { 116 | this.setState({ hasError: false }); 117 | } 118 | 119 | pureValidate() { 120 | return t.validate( 121 | this.getValue(), 122 | this.props.type, 123 | this.getValidationOptions() 124 | ); 125 | } 126 | 127 | validate() { 128 | const result = this.pureValidate(); 129 | this.setState({ hasError: !result.isValid() }); 130 | return result; 131 | } 132 | 133 | getAuto() { 134 | return this.props.options.auto || this.props.ctx.auto; 135 | } 136 | 137 | getI18n() { 138 | return this.props.options.i18n || this.props.ctx.i18n; 139 | } 140 | 141 | getDefaultLabel() { 142 | const ctx = this.props.ctx; 143 | if (ctx.label) { 144 | return ( 145 | ctx.label + 146 | (this.typeInfo.isMaybe 147 | ? this.getI18n().optional 148 | : this.getI18n().required) 149 | ); 150 | } 151 | } 152 | 153 | getLabel() { 154 | let label = this.props.options.label || this.props.options.legend; 155 | if (Nil.is(label) && this.getAuto() === "labels") { 156 | label = this.getDefaultLabel(); 157 | } 158 | return label; 159 | } 160 | 161 | getError() { 162 | if (this.hasError()) { 163 | const error = 164 | this.props.options.error || this.typeInfo.getValidationErrorMessage; 165 | if (t.Function.is(error)) { 166 | const validationOptions = this.getValidationOptions(); 167 | return error( 168 | this.getValue(), 169 | validationOptions.path, 170 | validationOptions.context 171 | ); 172 | } 173 | return error; 174 | } 175 | } 176 | 177 | hasError() { 178 | return this.props.options.hasError || this.state.hasError; 179 | } 180 | 181 | getConfig() { 182 | return merge(this.props.ctx.config, this.props.options.config); 183 | } 184 | 185 | getStylesheet() { 186 | return this.props.options.stylesheet || this.props.ctx.stylesheet; 187 | } 188 | 189 | getLocals() { 190 | return { 191 | path: this.props.ctx.path, 192 | error: this.getError(), 193 | hasError: this.hasError(), 194 | label: this.getLabel(), 195 | onChange: this.onChange.bind(this), 196 | config: this.getConfig(), 197 | value: this.state.value, 198 | hidden: this.props.options.hidden, 199 | stylesheet: this.getStylesheet() 200 | }; 201 | } 202 | 203 | render() { 204 | const locals = this.getLocals(); 205 | // getTemplate is the only required implementation when extending Component 206 | t.assert( 207 | t.Function.is(this.getTemplate), 208 | `[${SOURCE}] missing getTemplate method of component ${ 209 | this.constructor.name 210 | }` 211 | ); 212 | const template = this.getTemplate(); 213 | return template(locals); 214 | } 215 | } 216 | 217 | Component.transformer = { 218 | format: value => (Nil.is(value) ? null : value), 219 | parse: value => value 220 | }; 221 | 222 | function toNull(value) { 223 | return (t.String.is(value) && value.trim() === "") || Nil.is(value) 224 | ? null 225 | : value; 226 | } 227 | 228 | function parseNumber(value) { 229 | const n = parseFloat(value); 230 | const isNumeric = value - n + 1 >= 0; 231 | return isNumeric ? n : toNull(value); 232 | } 233 | 234 | class Textbox extends Component { 235 | getTransformer() { 236 | const options = this.props.options; 237 | return options.transformer 238 | ? options.transformer 239 | : this.typeInfo.innerType === t.Number 240 | ? Textbox.numberTransformer 241 | : Textbox.transformer; 242 | } 243 | 244 | getTemplate() { 245 | return this.props.options.template || this.props.ctx.templates.textbox; 246 | } 247 | 248 | getPlaceholder() { 249 | let placeholder = this.props.options.placeholder; 250 | if (Nil.is(placeholder) && this.getAuto() === "placeholders") { 251 | placeholder = this.getDefaultLabel(); 252 | } 253 | return placeholder; 254 | } 255 | 256 | getKeyboardType() { 257 | const keyboardType = this.props.options.keyboardType; 258 | if (t.Nil.is(keyboardType) && this.typeInfo.innerType === t.Number) { 259 | return "numeric"; 260 | } 261 | return keyboardType; 262 | } 263 | 264 | getLocals() { 265 | const locals = super.getLocals(); 266 | locals.placeholder = this.getPlaceholder(); 267 | locals.onChangeNative = this.props.options.onChange; 268 | locals.keyboardType = this.getKeyboardType(); 269 | locals.underlineColorAndroid = 270 | this.props.options.underlineColorAndroid || "transparent"; 271 | [ 272 | "help", 273 | "autoCapitalize", 274 | "autoCorrect", 275 | "autoFocus", 276 | "blurOnSubmit", 277 | "editable", 278 | "maxLength", 279 | "multiline", 280 | "onBlur", 281 | "onEndEditing", 282 | "onFocus", 283 | "onLayout", 284 | "onSelectionChange", 285 | "onSubmitEditing", 286 | "onContentSizeChange", 287 | "placeholderTextColor", 288 | "secureTextEntry", 289 | "selectTextOnFocus", 290 | "selectionColor", 291 | "numberOfLines", 292 | "clearButtonMode", 293 | "clearTextOnFocus", 294 | "enablesReturnKeyAutomatically", 295 | "keyboardAppearance", 296 | "onKeyPress", 297 | "returnKeyType", 298 | "selectionState" 299 | ].forEach(name => (locals[name] = this.props.options[name])); 300 | 301 | return locals; 302 | } 303 | } 304 | 305 | Textbox.transformer = { 306 | format: value => (Nil.is(value) ? "" : value), 307 | parse: toNull 308 | }; 309 | 310 | Textbox.numberTransformer = { 311 | format: value => (Nil.is(value) ? "" : String(value)), 312 | parse: parseNumber 313 | }; 314 | 315 | class Checkbox extends Component { 316 | getTemplate() { 317 | return this.props.options.template || this.props.ctx.templates.checkbox; 318 | } 319 | 320 | getLocals() { 321 | const locals = super.getLocals(); 322 | locals.label = 323 | this.props.ctx.auto !== "none" 324 | ? locals.label || this.getDefaultLabel() 325 | : null; 326 | ["help", "disabled", "onTintColor", "thumbTintColor", "tintColor"].forEach( 327 | name => (locals[name] = this.props.options[name]) 328 | ); 329 | 330 | return locals; 331 | } 332 | } 333 | 334 | Checkbox.transformer = { 335 | format: value => (Nil.is(value) ? false : value), 336 | parse: value => value 337 | }; 338 | 339 | class Select extends Component { 340 | getTransformer() { 341 | const options = this.props.options; 342 | if (options.transformer) { 343 | return options.transformer; 344 | } 345 | return Select.transformer(this.getNullOption()); 346 | } 347 | 348 | getTemplate() { 349 | return this.props.options.template || this.props.ctx.templates.select; 350 | } 351 | 352 | getNullOption() { 353 | return this.props.options.nullOption || { value: "", text: "-" }; 354 | } 355 | 356 | getEnum() { 357 | return this.typeInfo.innerType; 358 | } 359 | 360 | getOptions() { 361 | const options = this.props.options; 362 | const items = options.options 363 | ? options.options.slice() 364 | : getOptionsOfEnum(this.getEnum()); 365 | if (options.order) { 366 | items.sort(getComparator(options.order)); 367 | } 368 | const nullOption = this.getNullOption(); 369 | if (options.nullOption !== false) { 370 | items.unshift(nullOption); 371 | } 372 | return items; 373 | } 374 | 375 | getLocals() { 376 | const locals = super.getLocals(); 377 | locals.options = this.getOptions(); 378 | 379 | [ 380 | "help", 381 | "disabled", 382 | "mode", 383 | "prompt", 384 | "itemStyle", 385 | "isCollapsed", 386 | "onCollapseChange" 387 | ].forEach(name => (locals[name] = this.props.options[name])); 388 | 389 | return locals; 390 | } 391 | } 392 | 393 | Select.transformer = nullOption => { 394 | return { 395 | format: value => 396 | Nil.is(value) && nullOption ? nullOption.value : String(value), 397 | parse: value => (nullOption && nullOption.value === value ? null : value) 398 | }; 399 | }; 400 | 401 | class DatePicker extends Component { 402 | getTemplate() { 403 | return this.props.options.template || this.props.ctx.templates.datepicker; 404 | } 405 | 406 | getLocals() { 407 | const locals = super.getLocals(); 408 | [ 409 | "help", 410 | "disabled", 411 | "maximumDate", 412 | "minimumDate", 413 | "minuteInterval", 414 | "mode", 415 | "timeZoneOffsetInMinutes", 416 | "onPress" 417 | ].forEach(name => (locals[name] = this.props.options[name])); 418 | 419 | return locals; 420 | } 421 | } 422 | 423 | DatePicker.transformer = { 424 | format: value => (Nil.is(value) ? null : value), 425 | parse: value => value 426 | }; 427 | 428 | class Struct extends Component { 429 | isValueNully() { 430 | return Object.keys(this.refs).every(ref => this.refs[ref].isValueNully()); 431 | } 432 | 433 | removeErrors() { 434 | this.setState({ hasError: false }); 435 | Object.keys(this.refs).forEach(ref => this.refs[ref].removeErrors()); 436 | } 437 | 438 | getValue() { 439 | const value = {}; 440 | for (const ref in this.refs) { 441 | value[ref] = this.refs[ref].getValue(); 442 | } 443 | return this.getTransformer().parse(value); 444 | } 445 | 446 | validate() { 447 | let value = {}; 448 | let errors = []; 449 | let hasError = false; 450 | let result; 451 | 452 | if (this.typeInfo.isMaybe && this.isValueNully()) { 453 | this.removeErrors(); 454 | return new t.ValidationResult({ errors: [], value: null }); 455 | } 456 | 457 | for (const ref in this.refs) { 458 | if (this.refs.hasOwnProperty(ref)) { 459 | result = this.refs[ref].validate(); 460 | errors = errors.concat(result.errors); 461 | value[ref] = result.value; 462 | } 463 | } 464 | 465 | if (errors.length === 0) { 466 | const InnerType = this.typeInfo.innerType; 467 | value = new InnerType(value); 468 | if (this.typeInfo.isSubtype && errors.length === 0) { 469 | result = t.validate( 470 | value, 471 | this.props.type, 472 | this.getValidationOptions() 473 | ); 474 | hasError = !result.isValid(); 475 | errors = errors.concat(result.errors); 476 | } 477 | } 478 | 479 | this.setState({ hasError: hasError }); 480 | return new t.ValidationResult({ errors, value }); 481 | } 482 | 483 | onChange(fieldName, fieldValue, path) { 484 | const value = t.mixin({}, this.state.value); 485 | value[fieldName] = fieldValue; 486 | this.setState({ value }, () => { 487 | this.props.onChange(value, path); 488 | }); 489 | } 490 | 491 | getTemplates() { 492 | return merge(this.props.ctx.templates, this.props.options.templates); 493 | } 494 | 495 | getTemplate() { 496 | return this.props.options.template || this.getTemplates().struct; 497 | } 498 | 499 | getTypeProps() { 500 | return this.typeInfo.innerType.meta.props; 501 | } 502 | 503 | getOrder() { 504 | return this.props.options.order || Object.keys(this.getTypeProps()); 505 | } 506 | 507 | getInputs() { 508 | const { ctx, options } = this.props; 509 | const props = this.getTypeProps(); 510 | const auto = this.getAuto(); 511 | const i18n = this.getI18n(); 512 | const config = this.getConfig(); 513 | const value = this.state.value || {}; 514 | const templates = this.getTemplates(); 515 | const stylesheet = this.getStylesheet(); 516 | const inputs = {}; 517 | for (const prop in props) { 518 | if (props.hasOwnProperty(prop)) { 519 | const type = props[prop]; 520 | const propValue = value[prop]; 521 | const propType = getTypeFromUnion(type, propValue); 522 | const fieldsOptions = options.fields || noobj; 523 | const propOptions = getComponentOptions( 524 | fieldsOptions[prop], 525 | noobj, 526 | propValue, 527 | type 528 | ); 529 | inputs[prop] = React.createElement( 530 | getFormComponent(propType, propOptions), 531 | { 532 | key: prop, 533 | ref: prop, 534 | type: propType, 535 | options: propOptions, 536 | value: value[prop], 537 | onChange: this.onChange.bind(this, prop), 538 | ctx: { 539 | context: ctx.context, 540 | uidGenerator: ctx.uidGenerator, 541 | auto, 542 | config, 543 | label: humanize(prop), 544 | i18n, 545 | stylesheet, 546 | templates, 547 | path: this.props.ctx.path.concat(prop) 548 | } 549 | } 550 | ); 551 | } 552 | } 553 | return inputs; 554 | } 555 | 556 | getLocals() { 557 | const templates = this.getTemplates(); 558 | const locals = super.getLocals(); 559 | locals.order = this.getOrder(); 560 | locals.inputs = this.getInputs(); 561 | locals.template = templates.struct; 562 | return locals; 563 | } 564 | } 565 | 566 | function toSameLength(value, keys, uidGenerator) { 567 | if (value.length === keys.length) { 568 | return keys; 569 | } 570 | const ret = []; 571 | for (let i = 0, len = value.length; i < len; i++) { 572 | ret[i] = keys[i] || uidGenerator.next(); 573 | } 574 | return ret; 575 | } 576 | 577 | export class List extends Component { 578 | constructor(props) { 579 | super(props); 580 | this.state.keys = this.state.value.map(() => props.ctx.uidGenerator.next()); 581 | } 582 | 583 | componentWillReceiveProps(props) { 584 | if (props.type !== this.props.type) { 585 | this.typeInfo = getTypeInfo(props.type); 586 | } 587 | const value = this.getTransformer().format(props.value); 588 | this.setState({ 589 | value, 590 | keys: toSameLength(value, this.state.keys, props.ctx.uidGenerator) 591 | }); 592 | } 593 | 594 | isValueNully() { 595 | return this.state.value.length === 0; 596 | } 597 | 598 | removeErrors() { 599 | this.setState({ hasError: false }); 600 | Object.keys(this.refs).forEach(ref => this.refs[ref].removeErrors()); 601 | } 602 | 603 | getValue() { 604 | const value = []; 605 | for (let i = 0, len = this.state.value.length; i < len; i++) { 606 | if (this.refs.hasOwnProperty(i)) { 607 | value.push(this.refs[i].getValue()); 608 | } 609 | } 610 | return this.getTransformer().parse(value); 611 | } 612 | 613 | validate() { 614 | const value = []; 615 | let errors = []; 616 | let hasError = false; 617 | let result; 618 | 619 | if (this.typeInfo.isMaybe && this.isValueNully()) { 620 | this.removeErrors(); 621 | return new t.ValidationResult({ errors: [], value: null }); 622 | } 623 | 624 | for (let i = 0, len = this.state.value.length; i < len; i++) { 625 | result = this.refs[i].validate(); 626 | errors = errors.concat(result.errors); 627 | value.push(result.value); 628 | } 629 | 630 | // handle subtype 631 | if (this.typeInfo.isSubtype && errors.length === 0) { 632 | result = t.validate(value, this.props.type, this.getValidationOptions()); 633 | hasError = !result.isValid(); 634 | errors = errors.concat(result.errors); 635 | } 636 | 637 | this.setState({ hasError: hasError }); 638 | return new t.ValidationResult({ errors: errors, value: value }); 639 | } 640 | 641 | onChange(value, keys, path, kind) { 642 | const allkeys = toSameLength(value, keys, this.props.ctx.uidGenerator); 643 | this.setState({ value, keys: allkeys, isPristine: false }, () => { 644 | this.props.onChange(value, path, kind); 645 | }); 646 | } 647 | 648 | addItem() { 649 | const value = this.state.value.concat(undefined); 650 | const keys = this.state.keys.concat(this.props.ctx.uidGenerator.next()); 651 | this.onChange( 652 | value, 653 | keys, 654 | this.props.ctx.path.concat(value.length - 1), 655 | "add" 656 | ); 657 | } 658 | 659 | onItemChange(itemIndex, itemValue, path, kind) { 660 | const value = this.state.value.slice(); 661 | value[itemIndex] = itemValue; 662 | this.onChange(value, this.state.keys, path, kind); 663 | } 664 | 665 | removeItem(i) { 666 | const value = this.state.value.slice(); 667 | value.splice(i, 1); 668 | const keys = this.state.keys.slice(); 669 | keys.splice(i, 1); 670 | this.onChange(value, keys, this.props.ctx.path.concat(i), "remove"); 671 | } 672 | 673 | moveUpItem(i) { 674 | if (i > 0) { 675 | this.onChange( 676 | move(this.state.value.slice(), i, i - 1), 677 | move(this.state.keys.slice(), i, i - 1), 678 | this.props.ctx.path.concat(i), 679 | "moveUp" 680 | ); 681 | } 682 | } 683 | 684 | moveDownItem(i) { 685 | if (i < this.state.value.length - 1) { 686 | this.onChange( 687 | move(this.state.value.slice(), i, i + 1), 688 | move(this.state.keys.slice(), i, i + 1), 689 | this.props.ctx.path.concat(i), 690 | "moveDown" 691 | ); 692 | } 693 | } 694 | 695 | getTemplates() { 696 | return merge(this.props.ctx.templates, this.props.options.templates); 697 | } 698 | 699 | getTemplate() { 700 | return this.props.options.template || this.getTemplates().list; 701 | } 702 | 703 | getItems() { 704 | const { options, ctx } = this.props; 705 | const auto = this.getAuto(); 706 | const i18n = this.getI18n(); 707 | const config = this.getConfig(); 708 | const stylesheet = this.getStylesheet(); 709 | const templates = this.getTemplates(); 710 | const value = this.state.value; 711 | return value.map((itemValue, i) => { 712 | const type = this.typeInfo.innerType.meta.type; 713 | const itemType = getTypeFromUnion(type, itemValue); 714 | const itemOptions = getComponentOptions( 715 | options.item, 716 | noobj, 717 | itemValue, 718 | type 719 | ); 720 | const ItemComponent = getFormComponent(itemType, itemOptions); 721 | const buttons = []; 722 | if (!options.disableRemove) { 723 | buttons.push({ 724 | type: "remove", 725 | label: i18n.remove, 726 | click: this.removeItem.bind(this, i) 727 | }); 728 | } 729 | if (!options.disableOrder) { 730 | buttons.push( 731 | { 732 | type: "move-up", 733 | label: i18n.up, 734 | click: this.moveUpItem.bind(this, i) 735 | }, 736 | { 737 | type: "move-down", 738 | label: i18n.down, 739 | click: this.moveDownItem.bind(this, i) 740 | } 741 | ); 742 | } 743 | return { 744 | input: React.createElement(ItemComponent, { 745 | ref: i, 746 | type: itemType, 747 | options: itemOptions, 748 | value: itemValue, 749 | onChange: this.onItemChange.bind(this, i), 750 | ctx: { 751 | context: ctx.context, 752 | uidGenerator: ctx.uidGenerator, 753 | auto, 754 | config, 755 | label: ctx.label ? `${ctx.label}[${i + 1}]` : String(i + 1), 756 | i18n, 757 | stylesheet, 758 | templates, 759 | path: ctx.path.concat(i) 760 | } 761 | }), 762 | key: this.state.keys[i], 763 | buttons: buttons 764 | }; 765 | }); 766 | } 767 | 768 | getLocals() { 769 | const options = this.props.options; 770 | const i18n = this.getI18n(); 771 | const locals = super.getLocals(); 772 | locals.add = options.disableAdd 773 | ? null 774 | : { 775 | type: "add", 776 | label: i18n.add, 777 | click: this.addItem.bind(this) 778 | }; 779 | locals.items = this.getItems(); 780 | locals.className = options.className; 781 | return locals; 782 | } 783 | } 784 | 785 | List.transformer = { 786 | format: value => (Nil.is(value) ? noarr : value), 787 | parse: value => value 788 | }; 789 | 790 | class Form extends React.Component { 791 | pureValidate() { 792 | return this.refs.input.pureValidate(); 793 | } 794 | 795 | validate() { 796 | return this.refs.input.validate(); 797 | } 798 | 799 | getValue() { 800 | const result = this.validate(); 801 | return result.isValid() ? result.value : null; 802 | } 803 | 804 | getComponent(path) { 805 | path = t.String.is(path) ? path.split(".") : path; 806 | return path.reduce((input, name) => input.refs[name], this.refs.input); 807 | } 808 | 809 | getSeed() { 810 | const rii = this._reactInternalInstance; 811 | if (rii) { 812 | if (rii._hostContainerInfo) { 813 | return rii._hostContainerInfo._idCounter; 814 | } 815 | if (rii._nativeContainerInfo) { 816 | return rii._nativeContainerInfo._idCounter; 817 | } 818 | if (rii._rootNodeID) { 819 | return rii._rootNodeID; 820 | } 821 | } 822 | return "0"; 823 | } 824 | 825 | getUIDGenerator() { 826 | this.uidGenerator = this.uidGenerator || new UIDGenerator(this.getSeed()); 827 | return this.uidGenerator; 828 | } 829 | 830 | render() { 831 | const stylesheet = this.props.stylesheet || Form.stylesheet; 832 | const templates = this.props.templates || Form.templates; 833 | const i18n = this.props.i18n || Form.i18n; 834 | 835 | if (process.env.NODE_ENV !== "production") { 836 | t.assert( 837 | t.isType(this.props.type), 838 | `[${SOURCE}] missing required prop type` 839 | ); 840 | t.assert( 841 | t.maybe(t.Object).is(this.props.options) || 842 | t.Function.is(this.props.options) || 843 | t.list(t.maybe(t.Object)).is(this.props.options), 844 | `[${SOURCE}] prop options, if specified, must be an object, a function returning the options or a list of options for unions` 845 | ); 846 | t.assert( 847 | t.Object.is(stylesheet), 848 | `[${SOURCE}] missing stylesheet config` 849 | ); 850 | t.assert(t.Object.is(templates), `[${SOURCE}] missing templates config`); 851 | t.assert(t.Object.is(i18n), `[${SOURCE}] missing i18n config`); 852 | } 853 | 854 | const value = this.props.value; 855 | const type = getTypeFromUnion(this.props.type, value); 856 | const options = getComponentOptions( 857 | this.props.options, 858 | noobj, 859 | value, 860 | this.props.type 861 | ); 862 | 863 | // this is in the render method because I need this._reactInternalInstance._rootNodeID in React ^0.14.0 864 | // and this._reactInternalInstance._nativeContainerInfo._idCounter in React ^15.0.0 865 | const uidGenerator = this.getUIDGenerator(); 866 | 867 | return React.createElement(getFormComponent(type, options), { 868 | ref: "input", 869 | type: type, 870 | options: options, 871 | value: this.props.value, 872 | onChange: this.props.onChange || noop, 873 | ctx: { 874 | context: this.props.context, 875 | uidGenerator, 876 | auto: "labels", 877 | stylesheet, 878 | templates, 879 | i18n, 880 | path: [] 881 | } 882 | }); 883 | } 884 | } 885 | 886 | module.exports = { 887 | getComponent: getFormComponent, 888 | Component, 889 | Textbox, 890 | Checkbox, 891 | Select, 892 | DatePicker, 893 | Struct, 894 | List: List, 895 | Form 896 | }; 897 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var test = require("tape"); 4 | var t = require("tcomb-validation"); 5 | var bootstrap = { 6 | checkbox: function() {}, 7 | datepicker: function() {}, 8 | select: function() {}, 9 | struct: function() {}, 10 | textbox: function() {}, 11 | list: function() {} 12 | }; 13 | 14 | var core = require("../lib/components"); 15 | import { UIDGenerator } from "../lib/util"; 16 | 17 | var ctx = { 18 | auto: "labels", 19 | label: "ctxDefaultLabel", 20 | templates: bootstrap, 21 | i18n: { optional: " (optional)", required: "" }, 22 | uidGenerator: new UIDGenerator("seed"), 23 | path: [] 24 | }; 25 | var ctxPlaceholders = { 26 | auto: "placeholders", 27 | label: "ctxDefaultLabel", 28 | templates: bootstrap, 29 | i18n: { optional: " (optional)", required: "" }, 30 | uidGenerator: new UIDGenerator("seed"), 31 | path: [] 32 | }; 33 | var ctxNone = { 34 | auto: "none", 35 | label: "ctxDefaultLabel", 36 | templates: bootstrap, 37 | i18n: { optional: " (optional)", required: "" }, 38 | uidGenerator: new UIDGenerator("seed"), 39 | path: [] 40 | }; 41 | 42 | var Textbox = core.Textbox; 43 | 44 | test("Textbox:label", function(tape) { 45 | tape.plan(3); 46 | 47 | tape.strictEqual( 48 | new Textbox({ 49 | type: t.String, 50 | options: {}, 51 | ctx: ctx 52 | }).getLocals().label, 53 | "ctxDefaultLabel", 54 | "should have a default label" 55 | ); 56 | 57 | tape.strictEqual( 58 | new Textbox({ 59 | type: t.String, 60 | options: { label: "mylabel" }, 61 | ctx: ctx 62 | }).getLocals().label, 63 | "mylabel", 64 | "should handle `label` option" 65 | ); 66 | 67 | tape.strictEqual( 68 | new Textbox({ 69 | type: t.maybe(t.String), 70 | options: {}, 71 | ctx: ctx 72 | }).getLocals().label, 73 | "ctxDefaultLabel (optional)", 74 | "should handle optional types" 75 | ); 76 | }); 77 | 78 | test("Textbox:placeholder", function(tape) { 79 | tape.plan(6); 80 | 81 | tape.strictEqual( 82 | new Textbox({ 83 | type: t.String, 84 | options: {}, 85 | ctx: ctx 86 | }).getLocals().placeholder, 87 | undefined, 88 | "default placeholder should be undefined" 89 | ); 90 | 91 | tape.strictEqual( 92 | new Textbox({ 93 | type: t.String, 94 | options: { placeholder: "myplaceholder" }, 95 | ctx: ctx 96 | }).getLocals().placeholder, 97 | "myplaceholder", 98 | "should handle placeholder option" 99 | ); 100 | 101 | tape.strictEqual( 102 | new Textbox({ 103 | type: t.String, 104 | options: { label: "mylabel", placeholder: "myplaceholder" }, 105 | ctx: ctx 106 | }).getLocals().placeholder, 107 | "myplaceholder", 108 | "should handle placeholder option even if a label is specified" 109 | ); 110 | 111 | tape.strictEqual( 112 | new Textbox({ 113 | type: t.String, 114 | options: {}, 115 | ctx: ctxPlaceholders 116 | }).getLocals().placeholder, 117 | "ctxDefaultLabel", 118 | "should have a default placeholder if auto = placeholders" 119 | ); 120 | 121 | tape.strictEqual( 122 | new Textbox({ 123 | type: t.maybe(t.String), 124 | options: {}, 125 | ctx: ctxPlaceholders 126 | }).getLocals().placeholder, 127 | "ctxDefaultLabel (optional)", 128 | "should handle optional types if auto = placeholders" 129 | ); 130 | 131 | tape.strictEqual( 132 | new Textbox({ 133 | type: t.String, 134 | options: { placeholder: "myplaceholder" }, 135 | ctx: ctxNone 136 | }).getLocals().placeholder, 137 | "myplaceholder", 138 | "should handle placeholder option even if auto === none" 139 | ); 140 | }); 141 | 142 | test("Textbox:editable", function(tape) { 143 | tape.plan(3); 144 | 145 | tape.strictEqual( 146 | new Textbox({ 147 | type: t.String, 148 | options: {}, 149 | ctx: ctx 150 | }).getLocals().editable, 151 | undefined, 152 | "default editable should be undefined" 153 | ); 154 | 155 | tape.strictEqual( 156 | new Textbox({ 157 | type: t.String, 158 | options: { editable: true }, 159 | ctx: ctx 160 | }).getLocals().editable, 161 | true, 162 | "should handle editable = true" 163 | ); 164 | 165 | tape.strictEqual( 166 | new Textbox({ 167 | type: t.String, 168 | options: { editable: false }, 169 | ctx: ctx 170 | }).getLocals().editable, 171 | false, 172 | "should handle editable = false" 173 | ); 174 | }); 175 | 176 | test("Textbox:help", function(tape) { 177 | tape.plan(1); 178 | 179 | tape.strictEqual( 180 | new Textbox({ 181 | type: t.String, 182 | options: { help: "myhelp" }, 183 | ctx: ctx 184 | }).getLocals().help, 185 | "myhelp", 186 | "should handle help option" 187 | ); 188 | }); 189 | 190 | test("Textbox:value", function(tape) { 191 | tape.plan(3); 192 | 193 | tape.strictEqual( 194 | new Textbox({ 195 | type: t.String, 196 | options: {}, 197 | ctx: ctx 198 | }).getLocals().value, 199 | "", 200 | "default value should be empty string" 201 | ); 202 | 203 | tape.strictEqual( 204 | new Textbox({ 205 | type: t.String, 206 | options: {}, 207 | ctx: ctx, 208 | value: "a" 209 | }).getLocals().value, 210 | "a", 211 | "should handle value option" 212 | ); 213 | 214 | tape.strictEqual( 215 | new Textbox({ 216 | type: t.Number, 217 | options: {}, 218 | ctx: ctx, 219 | value: 1.1 220 | }).getLocals().value, 221 | "1.1", 222 | "should handle numeric values" 223 | ); 224 | }); 225 | 226 | test("Textbox:transformer", function(tape) { 227 | tape.plan(2); 228 | 229 | var transformer = { 230 | format: function(value) { 231 | return Array.isArray(value) ? value.join(" ") : value; 232 | }, 233 | parse: function(value) { 234 | return value.split(" "); 235 | } 236 | }; 237 | 238 | tape.strictEqual( 239 | new Textbox({ 240 | type: t.String, 241 | options: { transformer: transformer }, 242 | ctx: ctx, 243 | value: ["a", "b"] 244 | }).getLocals().value, 245 | "a b", 246 | "should handle transformer option (format)" 247 | ); 248 | 249 | tape.deepEqual( 250 | new Textbox({ 251 | type: t.String, 252 | options: { transformer: transformer }, 253 | ctx: ctx, 254 | value: ["a", "b"] 255 | }).pureValidate().value, 256 | ["a", "b"], 257 | "should handle transformer option (parse)" 258 | ); 259 | }); 260 | 261 | test("Textbox:hasError", function(tape) { 262 | tape.plan(4); 263 | 264 | tape.strictEqual( 265 | new Textbox({ 266 | type: t.String, 267 | options: {}, 268 | ctx: ctx 269 | }).getLocals().hasError, 270 | false, 271 | "default hasError should be false" 272 | ); 273 | 274 | tape.strictEqual( 275 | new Textbox({ 276 | type: t.String, 277 | options: { hasError: true }, 278 | ctx: ctx 279 | }).getLocals().hasError, 280 | true, 281 | "should handle hasError option" 282 | ); 283 | 284 | var textbox = new Textbox({ 285 | type: t.String, 286 | options: {}, 287 | ctx: ctx 288 | }); 289 | 290 | textbox.pureValidate(); 291 | 292 | tape.strictEqual( 293 | textbox.getLocals().hasError, 294 | false, 295 | "after a validation error hasError should be true" 296 | ); 297 | 298 | textbox = new Textbox({ 299 | type: t.String, 300 | options: {}, 301 | ctx: ctx, 302 | value: "a" 303 | }); 304 | 305 | textbox.pureValidate(); 306 | 307 | tape.strictEqual( 308 | textbox.getLocals().hasError, 309 | false, 310 | "after a validation success hasError should be false" 311 | ); 312 | }); 313 | 314 | test("Textbox:error", function(tape) { 315 | tape.plan(3); 316 | 317 | tape.strictEqual( 318 | new Textbox({ 319 | type: t.String, 320 | options: {}, 321 | ctx: ctx 322 | }).getLocals().error, 323 | undefined, 324 | "default error should be undefined" 325 | ); 326 | 327 | tape.strictEqual( 328 | new Textbox({ 329 | type: t.String, 330 | options: { error: "myerror", hasError: true }, 331 | ctx: ctx 332 | }).getLocals().error, 333 | "myerror", 334 | "should handle error option" 335 | ); 336 | 337 | tape.strictEqual( 338 | new Textbox({ 339 | type: t.String, 340 | options: { 341 | error: function(value) { 342 | return "error: " + value; 343 | }, 344 | hasError: true 345 | }, 346 | ctx: ctx, 347 | value: "a" 348 | }).getLocals().error, 349 | "error: a", 350 | "should handle error option as a function" 351 | ); 352 | }); 353 | 354 | test("Textbox:template", function(tape) { 355 | tape.plan(2); 356 | 357 | tape.strictEqual( 358 | new Textbox({ 359 | type: t.String, 360 | options: {}, 361 | ctx: ctx 362 | }).getTemplate(), 363 | bootstrap.textbox, 364 | "default template should be bootstrap.textbox" 365 | ); 366 | 367 | var template = function() {}; 368 | 369 | tape.strictEqual( 370 | new Textbox({ 371 | type: t.String, 372 | options: { template: template }, 373 | ctx: ctx 374 | }).getTemplate(), 375 | template, 376 | "should handle template option" 377 | ); 378 | }); 379 | 380 | var Select = core.Select; 381 | var Country = t.enums({ 382 | IT: "Italy", 383 | FR: "France", 384 | US: "United States" 385 | }); 386 | 387 | test("Select:label", function(tape) { 388 | tape.plan(3); 389 | 390 | tape.strictEqual( 391 | new Select({ 392 | type: Country, 393 | options: {}, 394 | ctx: ctx 395 | }).getLocals().label, 396 | "ctxDefaultLabel", 397 | "should have a default label" 398 | ); 399 | 400 | tape.strictEqual( 401 | new Select({ 402 | type: Country, 403 | options: { label: "mylabel" }, 404 | ctx: ctx 405 | }).getLocals().label, 406 | "mylabel", 407 | "should handle `label` option" 408 | ); 409 | 410 | tape.strictEqual( 411 | new Select({ 412 | type: t.maybe(Country), 413 | options: {}, 414 | ctx: ctx 415 | }).getLocals().label, 416 | "ctxDefaultLabel (optional)", 417 | "should handle optional types" 418 | ); 419 | }); 420 | 421 | test("Select:help", function(tape) { 422 | tape.plan(1); 423 | 424 | tape.strictEqual( 425 | new Select({ 426 | type: Country, 427 | options: { help: "myhelp" }, 428 | ctx: ctx 429 | }).getLocals().help, 430 | "myhelp", 431 | "should handle help option" 432 | ); 433 | }); 434 | 435 | test("Select:value", function(tape) { 436 | tape.plan(2); 437 | 438 | tape.strictEqual( 439 | new Select({ 440 | type: Country, 441 | options: {}, 442 | ctx: ctx 443 | }).getLocals().value, 444 | "", 445 | "default value should be nullOption.value" 446 | ); 447 | 448 | tape.strictEqual( 449 | new Select({ 450 | type: Country, 451 | options: {}, 452 | ctx: ctx, 453 | value: "a" 454 | }).getLocals().value, 455 | "a", 456 | "should handle value option" 457 | ); 458 | }); 459 | 460 | test("Select:transformer", function(tape) { 461 | tape.plan(2); 462 | 463 | var transformer = { 464 | format: function(value) { 465 | return t.String.is(value) ? value : value === true ? "1" : "0"; 466 | }, 467 | parse: function(value) { 468 | return value === "1"; 469 | } 470 | }; 471 | 472 | tape.strictEqual( 473 | new Select({ 474 | type: t.maybe(t.Boolean), 475 | options: { 476 | transformer: transformer, 477 | options: [{ value: "0", text: "No" }, { value: "1", text: "Yes" }] 478 | }, 479 | ctx: ctx, 480 | value: true 481 | }).getLocals().value, 482 | "1", 483 | "should handle transformer option (format)" 484 | ); 485 | 486 | tape.deepEqual( 487 | new Select({ 488 | type: t.maybe(t.Boolean), 489 | options: { 490 | transformer: transformer, 491 | options: [{ value: "0", text: "No" }, { value: "1", text: "Yes" }] 492 | }, 493 | ctx: ctx, 494 | value: true 495 | }).pureValidate().value, 496 | true, 497 | "should handle transformer option (parse)" 498 | ); 499 | }); 500 | 501 | test("Select:hasError", function(tape) { 502 | tape.plan(4); 503 | 504 | tape.strictEqual( 505 | new Select({ 506 | type: Country, 507 | options: {}, 508 | ctx: ctx 509 | }).getLocals().hasError, 510 | false, 511 | "default hasError should be false" 512 | ); 513 | 514 | tape.strictEqual( 515 | new Select({ 516 | type: Country, 517 | options: { hasError: true }, 518 | ctx: ctx 519 | }).getLocals().hasError, 520 | true, 521 | "should handle hasError option" 522 | ); 523 | 524 | var select = new Select({ 525 | type: Country, 526 | options: {}, 527 | ctx: ctx 528 | }); 529 | 530 | select.pureValidate(); 531 | 532 | tape.strictEqual( 533 | select.getLocals().hasError, 534 | false, 535 | "after a validation error hasError should be true" 536 | ); 537 | 538 | select = new Select({ 539 | type: Country, 540 | options: {}, 541 | ctx: ctx, 542 | value: "IT" 543 | }); 544 | 545 | select.pureValidate(); 546 | 547 | tape.strictEqual( 548 | select.getLocals().hasError, 549 | false, 550 | "after a validation success hasError should be false" 551 | ); 552 | }); 553 | 554 | test("Select:error", function(tape) { 555 | tape.plan(3); 556 | 557 | tape.strictEqual( 558 | new Select({ 559 | type: Country, 560 | options: {}, 561 | ctx: ctx 562 | }).getLocals().error, 563 | undefined, 564 | "default error should be undefined" 565 | ); 566 | 567 | tape.strictEqual( 568 | new Select({ 569 | type: Country, 570 | options: { error: "myerror", hasError: true }, 571 | ctx: ctx 572 | }).getLocals().error, 573 | "myerror", 574 | "should handle error option" 575 | ); 576 | 577 | tape.strictEqual( 578 | new Select({ 579 | type: Country, 580 | options: { 581 | error: function(value) { 582 | return "error: " + value; 583 | }, 584 | hasError: true 585 | }, 586 | ctx: ctx, 587 | value: "a" 588 | }).getLocals().error, 589 | "error: a", 590 | "should handle error option as a function" 591 | ); 592 | }); 593 | 594 | test("Select:template", function(tape) { 595 | tape.plan(2); 596 | 597 | tape.strictEqual( 598 | new Select({ 599 | type: Country, 600 | options: {}, 601 | ctx: ctx 602 | }).getTemplate(), 603 | bootstrap.select, 604 | "default template should be bootstrap.select" 605 | ); 606 | 607 | var template = function() {}; 608 | 609 | tape.strictEqual( 610 | new Select({ 611 | type: Country, 612 | options: { template: template }, 613 | ctx: ctx 614 | }).getTemplate(), 615 | template, 616 | "should handle template option" 617 | ); 618 | }); 619 | 620 | test("Select:options", function(tape) { 621 | tape.plan(1); 622 | 623 | tape.deepEqual( 624 | new Select({ 625 | type: Country, 626 | options: { 627 | options: [ 628 | { value: "IT", text: "Italia" }, 629 | { value: "US", text: "Stati Uniti" } 630 | ] 631 | }, 632 | ctx: ctx 633 | }).getLocals().options, 634 | [ 635 | { text: "-", value: "" }, 636 | { text: "Italia", value: "IT" }, 637 | { text: "Stati Uniti", value: "US" } 638 | ], 639 | "should handle options option" 640 | ); 641 | }); 642 | 643 | test("Select:order", function(tape) { 644 | tape.plan(2); 645 | 646 | tape.deepEqual( 647 | new Select({ 648 | type: Country, 649 | options: { order: "asc" }, 650 | ctx: ctx 651 | }).getLocals().options, 652 | [ 653 | { text: "-", value: "" }, 654 | { text: "France", value: "FR" }, 655 | { text: "Italy", value: "IT" }, 656 | { text: "United States", value: "US" } 657 | ], 658 | "should handle order = asc option" 659 | ); 660 | 661 | tape.deepEqual( 662 | new Select({ 663 | type: Country, 664 | options: { order: "desc" }, 665 | ctx: ctx 666 | }).getLocals().options, 667 | [ 668 | { text: "-", value: "" }, 669 | { text: "United States", value: "US" }, 670 | { text: "Italy", value: "IT" }, 671 | { text: "France", value: "FR" } 672 | ], 673 | "should handle order = desc option" 674 | ); 675 | }); 676 | 677 | test("Select:nullOption", function(tape) { 678 | tape.plan(2); 679 | 680 | tape.deepEqual( 681 | new Select({ 682 | type: Country, 683 | options: { 684 | nullOption: { value: "", text: "Select a country" } 685 | }, 686 | ctx: ctx 687 | }).getLocals().options, 688 | [ 689 | { value: "", text: "Select a country" }, 690 | { text: "Italy", value: "IT" }, 691 | { text: "France", value: "FR" }, 692 | { text: "United States", value: "US" } 693 | ], 694 | "should handle nullOption option" 695 | ); 696 | 697 | tape.deepEqual( 698 | new Select({ 699 | type: Country, 700 | options: { 701 | nullOption: false 702 | }, 703 | ctx: ctx, 704 | value: "US" 705 | }).getLocals().options, 706 | [ 707 | { text: "Italy", value: "IT" }, 708 | { text: "France", value: "FR" }, 709 | { text: "United States", value: "US" } 710 | ], 711 | "should skip the nullOption if nullOption = false" 712 | ); 713 | }); 714 | 715 | test("Select:isCollapsed:true", function(tape) { 716 | tape.plan(1); 717 | 718 | tape.strictEqual( 719 | new Select({ 720 | type: Country, 721 | options: { isCollapsed: true }, 722 | ctx: ctx 723 | }).getLocals().isCollapsed, 724 | true, 725 | "should handle help option" 726 | ); 727 | }); 728 | 729 | test("Select:isCollapsed:false", function(tape) { 730 | tape.plan(1); 731 | 732 | tape.strictEqual( 733 | new Select({ 734 | type: Country, 735 | options: { isCollapsed: false }, 736 | ctx: ctx 737 | }).getLocals().isCollapsed, 738 | false, 739 | "should handle help option" 740 | ); 741 | }); 742 | 743 | test("Select:onCollapse", function(tape) { 744 | tape.plan(1); 745 | const onCollapsFunc = () => {}; 746 | tape.strictEqual( 747 | new Select({ 748 | type: Country, 749 | options: { onCollapseChange: onCollapsFunc }, 750 | ctx: ctx 751 | }).getLocals().onCollapseChange, 752 | onCollapsFunc, 753 | "should handle help option" 754 | ); 755 | }); 756 | 757 | var Checkbox = core.Checkbox; 758 | 759 | test("Checkbox:label", function(tape) { 760 | tape.plan(3); 761 | 762 | tape.strictEqual( 763 | new Checkbox({ 764 | type: t.Boolean, 765 | options: {}, 766 | ctx: ctx 767 | }).getLocals().label, 768 | "ctxDefaultLabel", 769 | "should have a default label" 770 | ); 771 | 772 | tape.strictEqual( 773 | new Checkbox({ 774 | type: t.Boolean, 775 | options: { label: "mylabel" }, 776 | ctx: ctx 777 | }).getLocals().label, 778 | "mylabel", 779 | "should handle `label` option" 780 | ); 781 | 782 | tape.strictEqual( 783 | new Checkbox({ 784 | type: t.Boolean, 785 | options: {}, 786 | ctx: ctxNone 787 | }).getLocals().label, 788 | null, 789 | "should have null `label` when auto `none`" 790 | ); 791 | }); 792 | 793 | test("Checkbox:help", function(tape) { 794 | tape.plan(1); 795 | 796 | tape.strictEqual( 797 | new Checkbox({ 798 | type: t.Boolean, 799 | options: { help: "myhelp" }, 800 | ctx: ctx 801 | }).getLocals().help, 802 | "myhelp", 803 | "should handle help option" 804 | ); 805 | }); 806 | 807 | test("Checkbox:value", function(tape) { 808 | tape.plan(2); 809 | 810 | tape.strictEqual( 811 | new Checkbox({ 812 | type: t.Boolean, 813 | options: {}, 814 | ctx: ctx 815 | }).getLocals().value, 816 | false, 817 | "default value should be false" 818 | ); 819 | 820 | tape.strictEqual( 821 | new Checkbox({ 822 | type: t.Boolean, 823 | options: {}, 824 | ctx: ctx, 825 | value: true 826 | }).getLocals().value, 827 | true, 828 | "should handle value option" 829 | ); 830 | }); 831 | 832 | test("Checkbox:transformer", function(tape) { 833 | tape.plan(2); 834 | 835 | var transformer = { 836 | format: function(value) { 837 | return t.String.is(value) ? value : value === true ? "1" : "0"; 838 | }, 839 | parse: function(value) { 840 | return value === "1"; 841 | } 842 | }; 843 | 844 | tape.strictEqual( 845 | new Checkbox({ 846 | type: t.Boolean, 847 | options: { transformer: transformer }, 848 | ctx: ctx, 849 | value: true 850 | }).getLocals().value, 851 | "1", 852 | "should handle transformer option (format)" 853 | ); 854 | 855 | tape.deepEqual( 856 | new Checkbox({ 857 | type: t.Boolean, 858 | options: { transformer: transformer }, 859 | ctx: ctx, 860 | value: true 861 | }).pureValidate().value, 862 | true, 863 | "should handle transformer option (parse)" 864 | ); 865 | }); 866 | 867 | test("Checkbox:hasError", function(tape) { 868 | tape.plan(4); 869 | 870 | var True = t.subtype(t.Boolean, function(value) { 871 | return value === true; 872 | }); 873 | 874 | tape.strictEqual( 875 | new Checkbox({ 876 | type: True, 877 | options: {}, 878 | ctx: ctx 879 | }).getLocals().hasError, 880 | false, 881 | "default hasError should be false" 882 | ); 883 | 884 | tape.strictEqual( 885 | new Checkbox({ 886 | type: True, 887 | options: { hasError: true }, 888 | ctx: ctx 889 | }).getLocals().hasError, 890 | true, 891 | "should handle hasError option" 892 | ); 893 | 894 | var checkbox = new Checkbox({ 895 | type: True, 896 | options: {}, 897 | ctx: ctx 898 | }); 899 | 900 | checkbox.pureValidate(); 901 | 902 | tape.strictEqual( 903 | checkbox.getLocals().hasError, 904 | false, 905 | "after a validation error hasError should be true" 906 | ); 907 | 908 | checkbox = new Checkbox({ 909 | type: True, 910 | options: {}, 911 | ctx: ctx, 912 | value: true 913 | }); 914 | 915 | checkbox.pureValidate(); 916 | 917 | tape.strictEqual( 918 | checkbox.getLocals().hasError, 919 | false, 920 | "after a validation success hasError should be false" 921 | ); 922 | }); 923 | 924 | test("Checkbox:error", function(tape) { 925 | tape.plan(3); 926 | 927 | tape.strictEqual( 928 | new Checkbox({ 929 | type: t.Boolean, 930 | options: {}, 931 | ctx: ctx 932 | }).getLocals().error, 933 | undefined, 934 | "default error should be undefined" 935 | ); 936 | 937 | tape.strictEqual( 938 | new Checkbox({ 939 | type: t.Boolean, 940 | options: { error: "myerror", hasError: true }, 941 | ctx: ctx 942 | }).getLocals().error, 943 | "myerror", 944 | "should handle error option" 945 | ); 946 | 947 | tape.strictEqual( 948 | new Checkbox({ 949 | type: t.Boolean, 950 | options: { 951 | error: function(value) { 952 | return "error: " + value; 953 | }, 954 | hasError: true 955 | }, 956 | ctx: ctx, 957 | value: "a" 958 | }).getLocals().error, 959 | "error: a", 960 | "should handle error option as a function" 961 | ); 962 | }); 963 | 964 | test("Checkbox:template", function(tape) { 965 | tape.plan(2); 966 | 967 | tape.strictEqual( 968 | new Checkbox({ 969 | type: t.Boolean, 970 | options: {}, 971 | ctx: ctx 972 | }).getTemplate(), 973 | bootstrap.checkbox, 974 | "default template should be bootstrap.checkbox" 975 | ); 976 | 977 | var template = function() {}; 978 | 979 | tape.strictEqual( 980 | new Checkbox({ 981 | type: t.Boolean, 982 | options: { template: template }, 983 | ctx: ctx 984 | }).getTemplate(), 985 | template, 986 | "should handle template option" 987 | ); 988 | }); 989 | 990 | var DatePicker = core.DatePicker; 991 | var date = new Date(1973, 10, 30); 992 | 993 | test("DatePicker:label", function(tape) { 994 | tape.plan(2); 995 | 996 | tape.strictEqual( 997 | new DatePicker({ 998 | type: t.Date, 999 | options: {}, 1000 | ctx: ctx, 1001 | value: date 1002 | }).getLocals().label, 1003 | "ctxDefaultLabel", 1004 | "should have a default label" 1005 | ); 1006 | 1007 | tape.strictEqual( 1008 | new DatePicker({ 1009 | type: t.Date, 1010 | options: { label: "mylabel" }, 1011 | ctx: ctx, 1012 | value: date 1013 | }).getLocals().label, 1014 | "mylabel", 1015 | "should handle `label` option" 1016 | ); 1017 | }); 1018 | 1019 | test("DatePicker:help", function(tape) { 1020 | tape.plan(1); 1021 | 1022 | tape.strictEqual( 1023 | new DatePicker({ 1024 | type: t.Date, 1025 | options: { help: "myhelp" }, 1026 | ctx: ctx, 1027 | value: date 1028 | }).getLocals().help, 1029 | "myhelp", 1030 | "should handle help option" 1031 | ); 1032 | }); 1033 | 1034 | test("DatePicker:value", function(tape) { 1035 | tape.plan(1); 1036 | 1037 | tape.strictEqual( 1038 | new DatePicker({ 1039 | type: t.Date, 1040 | options: {}, 1041 | ctx: ctx, 1042 | value: date 1043 | }).getLocals().value, 1044 | date, 1045 | "should handle value option" 1046 | ); 1047 | }); 1048 | 1049 | test("DatePicker:transformer", function(tape) { 1050 | tape.plan(2); 1051 | 1052 | var transformer = { 1053 | format: function(value) { 1054 | return Array.isArray(value) 1055 | ? value 1056 | : [value.getFullYear(), value.getMonth(), value.getDate()]; 1057 | }, 1058 | parse: function(value) { 1059 | return new Date(value[0], value[1], value[2]); 1060 | } 1061 | }; 1062 | 1063 | tape.deepEqual( 1064 | new DatePicker({ 1065 | type: t.String, 1066 | options: { transformer: transformer }, 1067 | ctx: ctx, 1068 | value: date 1069 | }).getLocals().value, 1070 | [1973, 10, 30], 1071 | "should handle transformer option (format)" 1072 | ); 1073 | 1074 | tape.deepEqual( 1075 | transformer.format( 1076 | new DatePicker({ 1077 | type: t.Date, 1078 | options: { transformer: transformer }, 1079 | ctx: ctx, 1080 | value: [1973, 10, 30] 1081 | }).pureValidate().value 1082 | ), 1083 | [1973, 10, 30], 1084 | "should handle transformer option (parse)" 1085 | ); 1086 | }); 1087 | 1088 | test("DatePicker:hasError", function(tape) { 1089 | tape.plan(4); 1090 | 1091 | tape.strictEqual( 1092 | new DatePicker({ 1093 | type: t.Date, 1094 | options: {}, 1095 | ctx: ctx, 1096 | value: date 1097 | }).getLocals().hasError, 1098 | false, 1099 | "default hasError should be false" 1100 | ); 1101 | 1102 | tape.strictEqual( 1103 | new DatePicker({ 1104 | type: t.Date, 1105 | options: { hasError: true }, 1106 | ctx: ctx, 1107 | value: date 1108 | }).getLocals().hasError, 1109 | true, 1110 | "should handle hasError option" 1111 | ); 1112 | 1113 | var datePicker = new DatePicker({ 1114 | type: t.Date, 1115 | options: {}, 1116 | ctx: ctx, 1117 | value: date 1118 | }); 1119 | 1120 | datePicker.pureValidate(); 1121 | 1122 | tape.strictEqual( 1123 | datePicker.getLocals().hasError, 1124 | false, 1125 | "after a validation error hasError should be true" 1126 | ); 1127 | 1128 | datePicker = new DatePicker({ 1129 | type: t.Date, 1130 | options: {}, 1131 | ctx: ctx, 1132 | value: date 1133 | }); 1134 | 1135 | datePicker.pureValidate(); 1136 | 1137 | tape.strictEqual( 1138 | datePicker.getLocals().hasError, 1139 | false, 1140 | "after a validation success hasError should be false" 1141 | ); 1142 | }); 1143 | 1144 | test("DatePicker:error", function(tape) { 1145 | tape.plan(3); 1146 | 1147 | tape.strictEqual( 1148 | new DatePicker({ 1149 | type: t.Date, 1150 | options: {}, 1151 | ctx: ctx, 1152 | value: date 1153 | }).getLocals().error, 1154 | undefined, 1155 | "default error should be undefined" 1156 | ); 1157 | 1158 | tape.strictEqual( 1159 | new DatePicker({ 1160 | type: t.Date, 1161 | options: { error: "myerror", hasError: true }, 1162 | ctx: ctx, 1163 | value: date 1164 | }).getLocals().error, 1165 | "myerror", 1166 | "should handle error option" 1167 | ); 1168 | 1169 | tape.strictEqual( 1170 | new DatePicker({ 1171 | type: t.Date, 1172 | options: { 1173 | error: function(value) { 1174 | return "error: " + value.getFullYear(); 1175 | }, 1176 | hasError: true 1177 | }, 1178 | ctx: ctx, 1179 | value: date 1180 | }).getLocals().error, 1181 | "error: 1973", 1182 | "should handle error option as a function" 1183 | ); 1184 | }); 1185 | 1186 | test("DatePicker:template", function(tape) { 1187 | tape.plan(2); 1188 | 1189 | tape.strictEqual( 1190 | new DatePicker({ 1191 | type: t.Date, 1192 | options: {}, 1193 | ctx: ctx, 1194 | value: date 1195 | }).getTemplate(), 1196 | bootstrap.datepicker, 1197 | "default template should be bootstrap.datepicker" 1198 | ); 1199 | 1200 | var template = function() {}; 1201 | 1202 | tape.strictEqual( 1203 | new DatePicker({ 1204 | type: t.Date, 1205 | options: { template: template }, 1206 | ctx: ctx, 1207 | value: date 1208 | }).getTemplate(), 1209 | template, 1210 | "should handle template option" 1211 | ); 1212 | }); 1213 | 1214 | var List = core.List; 1215 | 1216 | test("List:should support unions", assert => { 1217 | assert.plan(2); 1218 | 1219 | const AccountType = t.enums.of(["type 1", "type 2", "other"], "AccountType"); 1220 | 1221 | const KnownAccount = t.struct( 1222 | { 1223 | type: AccountType 1224 | }, 1225 | "KnownAccount" 1226 | ); 1227 | 1228 | const UnknownAccount = KnownAccount.extend( 1229 | { 1230 | label: t.String 1231 | }, 1232 | "UnknownAccount" 1233 | ); 1234 | 1235 | const Account = t.union([KnownAccount, UnknownAccount], "Account"); 1236 | 1237 | Account.dispatch = value => 1238 | value && value.type === "other" ? UnknownAccount : KnownAccount; 1239 | 1240 | let component = new List({ 1241 | type: t.list(Account), 1242 | ctx: ctx, 1243 | options: {}, 1244 | value: [{ type: "type 1" }] 1245 | }); 1246 | 1247 | assert.strictEqual(component.getItems()[0].input.props.type, KnownAccount); 1248 | 1249 | component = new List({ 1250 | type: t.list(Account), 1251 | ctx: ctx, 1252 | options: {}, 1253 | value: [{ type: "other" }] 1254 | }); 1255 | 1256 | assert.strictEqual(component.getItems()[0].input.props.type, UnknownAccount); 1257 | }); 1258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://img.shields.io/travis/gcanti/tcomb-form-native/master.svg?style=flat-square)](https://travis-ci.org/gcanti/tcomb-form-native) 2 | [![dependency status](https://img.shields.io/david/gcanti/tcomb-form-native.svg?style=flat-square)](https://david-dm.org/gcanti/tcomb-form-native) 3 | ![npm downloads](https://img.shields.io/npm/dm/tcomb-form-native.svg) 4 | 5 | # Notice 6 | 7 | `tcomb-form-native` is looking for maintainers. If you're interested in helping, a great way to get started would just be to start weighing-in on [GitHub issues](https://github.com/gcanti/tcomb-form-native/issues), reviewing and testing some [PRs](https://github.com/gcanti/tcomb-form-native/pulls). 8 | 9 | # Contents 10 | 11 | - [Setup](#setup) 12 | - [Supported react-native versions](#supported-react-native-versions) 13 | - [Example](#example) 14 | - [API](#api) 15 | - [Types](#types) 16 | - [Rendering options](#rendering-options) 17 | - [Unions](#unions) 18 | - [Lists](#lists) 19 | - [Customizations](#customizations) 20 | - [Tests](#tests) 21 | - [License](#license) 22 | 23 | # Setup 24 | 25 | ``` 26 | npm install tcomb-form-native 27 | ``` 28 | 29 | # Supported react-native versions 30 | 31 | | Version | React Native Support | Android Support | iOS Support | 32 | |---|---|---|---| 33 | | 0.5 - 0.6.1 | 0.25.0 - 0.35.0 | 7.1 | 10.0.2 | 34 | | 0.4 | 0.20.0 - 0.24.0 | 7.1 | 10.0.2 | 35 | | 0.3 | 0.1.0 - 0.13.0 | 7.1 | 10.0.2 | 36 | *Complies with [react-native-version-support-table](https://github.com/dangnelson/react-native-version-support-table)* 37 | 38 | ### Domain Driven Forms 39 | 40 | The [tcomb library](https://github.com/gcanti/tcomb) provides a concise but expressive way to define domain models in JavaScript. 41 | 42 | The [tcomb-validation library](https://github.com/gcanti/tcomb-validation) builds on tcomb, providing validation functions for tcomb domain models. 43 | 44 | This library builds on those two and the awesome react-native. 45 | 46 | ### Benefits 47 | 48 | With **tcomb-form-native** you simply call `
` to generate a form based on that domain model. What does this get you? 49 | 50 | 1. Write a lot less code 51 | 2. Usability and accessibility for free (automatic labels, inline validation, etc) 52 | 3. No need to update forms when domain model changes 53 | 54 | ### JSON Schema support 55 | 56 | JSON Schemas are also supported via the (tiny) [tcomb-json-schema library](https://github.com/gcanti/tcomb-json-schema). 57 | 58 | **Note**. Please use tcomb-json-schema ^0.2.5. 59 | 60 | ### Pluggable look and feel 61 | 62 | The look and feel is customizable via react-native stylesheets and *templates* (see documentation). 63 | 64 | ### Screencast 65 | 66 | http://react.rocks/example/tcomb-form-native 67 | 68 | ### Example App 69 | 70 | [https://github.com/bartonhammond/snowflake](https://github.com/bartonhammond/snowflake) React-Native, Tcomb, Redux, Parse.com, Jest - 88% coverage 71 | 72 | # Example 73 | 74 | ```js 75 | // index.ios.js 76 | 77 | 'use strict'; 78 | 79 | var React = require('react-native'); 80 | var t = require('tcomb-form-native'); 81 | var { AppRegistry, StyleSheet, Text, View, TouchableHighlight } = React; 82 | 83 | var Form = t.form.Form; 84 | 85 | // here we are: define your domain model 86 | var Person = t.struct({ 87 | name: t.String, // a required string 88 | surname: t.maybe(t.String), // an optional string 89 | age: t.Number, // a required number 90 | rememberMe: t.Boolean // a boolean 91 | }); 92 | 93 | var options = {}; // optional rendering options (see documentation) 94 | 95 | var AwesomeProject = React.createClass({ 96 | 97 | onPress: function () { 98 | // call getValue() to get the values of the form 99 | var value = this.refs.form.getValue(); 100 | if (value) { // if validation fails, value will be null 101 | console.log(value); // value here is an instance of Person 102 | } 103 | }, 104 | 105 | render: function() { 106 | return ( 107 | 108 | {/* display */} 109 | 114 | 115 | Save 116 | 117 | 118 | ); 119 | } 120 | }); 121 | 122 | var styles = StyleSheet.create({ 123 | container: { 124 | justifyContent: 'center', 125 | marginTop: 50, 126 | padding: 20, 127 | backgroundColor: '#ffffff', 128 | }, 129 | buttonText: { 130 | fontSize: 18, 131 | color: 'white', 132 | alignSelf: 'center' 133 | }, 134 | button: { 135 | height: 36, 136 | backgroundColor: '#48BBEC', 137 | borderColor: '#48BBEC', 138 | borderWidth: 1, 139 | borderRadius: 8, 140 | marginBottom: 10, 141 | alignSelf: 'stretch', 142 | justifyContent: 'center' 143 | } 144 | }); 145 | 146 | AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); 147 | ``` 148 | 149 | **Output:** 150 | 151 | (Labels are automatically generated) 152 | 153 | ![Result](docs/images/result.png) 154 | 155 | **Ouput after a validation error:** 156 | 157 | ![Result after a validation error](docs/images/validation.png) 158 | 159 | # API 160 | 161 | ## `getValue()` 162 | 163 | Returns `null` if the validation failed, an instance of your model otherwise. 164 | 165 | > **Note**. Calling `getValue` will cause the validation of all the fields of the form, including some side effects like highlighting the errors. 166 | 167 | ## `validate()` 168 | 169 | Returns a `ValidationResult` (see [tcomb-validation](https://github.com/gcanti/tcomb-validation) for a reference documentation). 170 | 171 | ## Adding a default value and listen to changes 172 | 173 | The `Form` component behaves like a [controlled component](https://facebook.github.io/react/docs/forms.html): 174 | 175 | ```js 176 | var Person = t.struct({ 177 | name: t.String, 178 | surname: t.maybe(t.String) 179 | }); 180 | 181 | var AwesomeProject = React.createClass({ 182 | 183 | getInitialState() { 184 | return { 185 | value: { 186 | name: 'Giulio', 187 | surname: 'Canti' 188 | } 189 | }; 190 | }, 191 | 192 | onChange(value) { 193 | this.setState({value}); 194 | }, 195 | 196 | onPress: function () { 197 | var value = this.refs.form.getValue(); 198 | if (value) { 199 | console.log(value); 200 | } 201 | }, 202 | 203 | render: function() { 204 | return ( 205 | 206 | 212 | 213 | Save 214 | 215 | 216 | ); 217 | } 218 | }); 219 | ``` 220 | 221 | The `onChange` handler has the following signature: 222 | 223 | ``` 224 | (raw: any, path: Array) => void 225 | ``` 226 | 227 | where 228 | 229 | - `raw` contains the current raw value of the form (can be an invalid value for your model) 230 | - `path` is the path to the field triggering the change 231 | 232 | > **Warning**. tcomb-form-native uses `shouldComponentUpdate` aggressively. In order to ensure that tcomb-form-native detect any change to `type`, `options` or `value` props you have to change references: 233 | 234 | ## Disable a field based on another field's value 235 | 236 | ```js 237 | var Type = t.struct({ 238 | disable: t.Boolean, // if true, name field will be disabled 239 | name: t.String 240 | }); 241 | 242 | // see the "Rendering options" section in this guide 243 | var options = { 244 | fields: { 245 | name: {} 246 | } 247 | }; 248 | 249 | var AwesomeProject = React.createClass({ 250 | 251 | getInitialState() { 252 | return { 253 | options: options, 254 | value: null 255 | }; 256 | }, 257 | 258 | onChange(value) { 259 | // tcomb immutability helpers 260 | // https://github.com/gcanti/tcomb/blob/master/docs/API.md#updating-immutable-instances 261 | var options = t.update(this.state.options, { 262 | fields: { 263 | name: { 264 | editable: {'$set': !value.disable} 265 | } 266 | } 267 | }); 268 | this.setState({options: options, value: value}); 269 | }, 270 | 271 | onPress: function () { 272 | var value = this.refs.form.getValue(); 273 | if (value) { 274 | console.log(value); 275 | } 276 | }, 277 | 278 | render: function() { 279 | return ( 280 | 281 | 288 | 289 | Save 290 | 291 | 292 | ); 293 | } 294 | 295 | }); 296 | ``` 297 | 298 | ## How to get access to a field 299 | 300 | You can get access to a field with the `getComponent(path)` API: 301 | 302 | ```js 303 | var Person = t.struct({ 304 | name: t.String, 305 | surname: t.maybe(t.String), 306 | age: t.Number, 307 | rememberMe: t.Boolean 308 | }); 309 | 310 | var AwesomeProject = React.createClass({ 311 | 312 | componentDidMount() { 313 | // give focus to the name textbox 314 | this.refs.form.getComponent('name').refs.input.focus(); 315 | }, 316 | 317 | onPress: function () { 318 | var value = this.refs.form.getValue(); 319 | if (value) { 320 | console.log(value); 321 | } 322 | }, 323 | 324 | render: function() { 325 | return ( 326 | 327 | 331 | 332 | Save 333 | 334 | 335 | ); 336 | } 337 | }); 338 | ``` 339 | 340 | ## How to clear form after submit 341 | 342 | ```js 343 | var Person = t.struct({ 344 | name: t.String, 345 | surname: t.maybe(t.String), 346 | age: t.Number, 347 | rememberMe: t.Boolean 348 | }); 349 | 350 | var AwesomeProject = React.createClass({ 351 | 352 | getInitialState() { 353 | return { value: null }; 354 | }, 355 | 356 | onChange(value) { 357 | this.setState({ value }); 358 | }, 359 | 360 | clearForm() { 361 | // clear content from all textbox 362 | this.setState({ value: null }); 363 | }, 364 | 365 | onPress: function () { 366 | var value = this.refs.form.getValue(); 367 | if (value) { 368 | console.log(value); 369 | // clear all fields after submit 370 | this.clearForm(); 371 | } 372 | }, 373 | 374 | render: function() { 375 | return ( 376 | 377 | 383 | 384 | Save 385 | 386 | 387 | ); 388 | } 389 | }); 390 | ``` 391 | 392 | ## Dynamic forms example: how to change a form based on selection 393 | 394 | Say I have an iOS Picker, depending on which option is selected in this picker I want the next component to either be a checkbox or a textbox: 395 | 396 | ```js 397 | const Country = t.enums({ 398 | 'IT': 'Italy', 399 | 'US': 'United States' 400 | }, 'Country'); 401 | 402 | var AwesomeProject = React.createClass({ 403 | 404 | // returns the suitable type based on the form value 405 | getType(value) { 406 | if (value.country === 'IT') { 407 | return t.struct({ 408 | country: Country, 409 | rememberMe: t.Boolean 410 | }); 411 | } else if (value.country === 'US') { 412 | return t.struct({ 413 | country: Country, 414 | name: t.String 415 | }); 416 | } else { 417 | return t.struct({ 418 | country: Country 419 | }); 420 | } 421 | }, 422 | 423 | getInitialState() { 424 | const value = {}; 425 | return { value, type: this.getType(value) }; 426 | }, 427 | 428 | onChange(value) { 429 | // recalculate the type only if strictly necessary 430 | const type = value.country !== this.state.value.country ? 431 | this.getType(value) : 432 | this.state.type; 433 | this.setState({ value, type }); 434 | }, 435 | 436 | onPress() { 437 | var value = this.refs.form.getValue(); 438 | if (value) { 439 | console.log(value); 440 | } 441 | }, 442 | 443 | render() { 444 | 445 | return ( 446 | 447 | 453 | 454 | Save 455 | 456 | 457 | ); 458 | } 459 | }); 460 | ``` 461 | 462 | # Types 463 | 464 | ### Required field 465 | 466 | By default fields are required: 467 | 468 | ```js 469 | var Person = t.struct({ 470 | name: t.String, // a required string 471 | surname: t.String // a required string 472 | }); 473 | ``` 474 | 475 | ### Optional field 476 | 477 | In order to create an optional field, wrap the field type with the `t.maybe` combinator: 478 | 479 | ```js 480 | var Person = t.struct({ 481 | name: t.String, 482 | surname: t.String, 483 | email: t.maybe(t.String) // an optional string 484 | }); 485 | ``` 486 | 487 | The postfix `" (optional)"` is automatically added to optional fields. 488 | 489 | You can customise the postfix value or setting a postfix for required fields: 490 | 491 | ```js 492 | t.form.Form.i18n = { 493 | optional: '', 494 | required: ' (required)' // inverting the behaviour: adding a postfix to the required fields 495 | }; 496 | ``` 497 | 498 | ### Numbers 499 | 500 | In order to create a numeric field, use the `t.Number` type: 501 | 502 | ```js 503 | var Person = t.struct({ 504 | name: t.String, 505 | surname: t.String, 506 | email: t.maybe(t.String), 507 | age: t.Number // a numeric field 508 | }); 509 | ``` 510 | 511 | tcomb-form-native will convert automatically numbers to / from strings. 512 | 513 | ### Booleans 514 | 515 | In order to create a boolean field, use the `t.Boolean` type: 516 | 517 | ```js 518 | var Person = t.struct({ 519 | name: t.String, 520 | surname: t.String, 521 | email: t.maybe(t.String), 522 | age: t.Number, 523 | rememberMe: t.Boolean // a boolean field 524 | }); 525 | ``` 526 | 527 | Booleans are displayed as `SwitchIOS`s. 528 | 529 | ### Dates 530 | 531 | In order to create a date field, use the `t.Date` type: 532 | 533 | ```js 534 | var Person = t.struct({ 535 | name: t.String, 536 | surname: t.String, 537 | email: t.maybe(t.String), 538 | age: t.Number, 539 | birthDate: t.Date // a date field 540 | }); 541 | ``` 542 | 543 | Dates are displayed as `DatePickerIOS`s under iOS and `DatePickerAndroid` or `TimePickerAndroid` under Android, depending on the `mode` selected (`date` or `time`). 544 | 545 | #### iOS date `config` option 546 | 547 | The bundled template will render an iOS `UIDatePicker` component, but collapsed into a touchable component in order to improve usability. A `config` object can be passed to customize it with the following parameters: 548 | 549 | | Key | Value | 550 | |-----|-------| 551 | | `animation` | The animation to collapse the date picker. Defaults to `Animated.timing`. | 552 | | `animationConfig` | The animation configuration object. Defaults to `{duration: 200}` for the default animation. | 553 | | `format` | A `(date) => String(date)` kind of function to provide a custom date format parsing to display the value. Optional, defaults to `(date) => String(date)`. 554 | | `defaultValueText` | An `string` to customize the default value of the `null` date value text. | 555 | 556 | For the collapsible customization, look at the `dateTouchable` and `dateValue` keys in the stylesheet file. 557 | 558 | #### Android date `config` option 559 | 560 | When using a `t.Date` type in Android, it can be configured through a `config` option that take the following parameters: 561 | 562 | | Key | Value | 563 | |-----|-------| 564 | | ``background`` | Determines the type of background drawable that's going to be used to display feedback. Optional, defaults to ``TouchableNativeFeedback.SelectableBackground``. | 565 | | ``format`` | A ``(date) => String(date)`` kind of function to provide a custom date format parsing to display the value. Optional, defaults to ``(date) => String(date)``. 566 | | ``dialogMode`` | Determines the type of datepicker mode for Android (`default`, `spinner` or `calendar`). | 567 | | `defaultValueText` | An `string` to customize the default value of the `null` date value text. | 568 | 569 | ### Enums 570 | 571 | In order to create an enum field, use the `t.enums` combinator: 572 | 573 | ```js 574 | var Gender = t.enums({ 575 | M: 'Male', 576 | F: 'Female' 577 | }); 578 | 579 | var Person = t.struct({ 580 | name: t.String, 581 | surname: t.String, 582 | email: t.maybe(t.String), 583 | age: t.Number, 584 | rememberMe: t.Boolean, 585 | gender: Gender // enum 586 | }); 587 | ``` 588 | 589 | Enums are displayed as `Picker`s. 590 | 591 | #### iOS select `config` option 592 | 593 | The bundled template will render an iOS `UIPickerView` component, but collapsed into a touchable component in order to improve usability. A `config` object can be passed to customize it with the following parameters: 594 | 595 | | Key | Value | 596 | |-----|-------| 597 | | `animation` | The animation to collapse the date picker. Defaults to `Animated.timing`. | 598 | | `animationConfig` | The animation configuration object. Defaults to `{duration: 200}` for the default animation. | 599 | 600 | For the collapsible customization, look at the `pickerContainer`, `pickerTouchable` and `pickerValue` keys in the stylesheet file. 601 | 602 | ### Refinements 603 | 604 | A *predicate* is a function with the following signature: 605 | 606 | ``` 607 | (x: any) => boolean 608 | ``` 609 | 610 | You can refine a type with the `t.refinement(type, predicate)` combinator: 611 | 612 | ```js 613 | // a type representing positive numbers 614 | var Positive = t.refinement(t.Number, function (n) { 615 | return n >= 0; 616 | }); 617 | 618 | var Person = t.struct({ 619 | name: t.String, 620 | surname: t.String, 621 | email: t.maybe(t.String), 622 | age: Positive, // refinement 623 | rememberMe: t.Boolean, 624 | gender: Gender 625 | }); 626 | ``` 627 | 628 | Subtypes allow you to express any custom validation with a simple predicate. 629 | 630 | # Rendering options 631 | 632 | In order to customize the look and feel, use an `options` prop: 633 | 634 | ```js 635 | 636 | ``` 637 | 638 | ## Form component 639 | 640 | ### Labels and placeholders 641 | 642 | By default labels are automatically generated. You can turn off this behaviour or override the default labels 643 | on field basis. 644 | 645 | ```js 646 | var options = { 647 | label: 'My struct label' // <= form legend, displayed before the fields 648 | }; 649 | 650 | var options = { 651 | fields: { 652 | name: { 653 | label: 'My name label' // <= label for the name field 654 | } 655 | } 656 | }; 657 | ``` 658 | 659 | In order to automatically generate default placeholders, use the option `auto: 'placeholders'`: 660 | 661 | ```js 662 | var options = { 663 | auto: 'placeholders' 664 | }; 665 | 666 | 667 | ``` 668 | 669 | ![Placeholders](docs/images/placeholders.png) 670 | 671 | Set `auto: 'none'` if you don't want neither labels nor placeholders. 672 | 673 | ```js 674 | var options = { 675 | auto: 'none' 676 | }; 677 | ``` 678 | 679 | ### Fields order 680 | 681 | You can sort the fields with the `order` option: 682 | 683 | ```js 684 | var options = { 685 | order: ['name', 'surname', 'rememberMe', 'gender', 'age', 'email'] 686 | }; 687 | ``` 688 | 689 | ### Default values 690 | 691 | You can set the default values passing a `value` prop to the `Form` component: 692 | 693 | ```js 694 | var value = { 695 | name: 'Giulio', 696 | surname: 'Canti', 697 | age: 41, 698 | gender: 'M' 699 | }; 700 | 701 | 702 | ``` 703 | 704 | ### Fields configuration 705 | 706 | You can configure each field with the `fields` option: 707 | 708 | ```js 709 | var options = { 710 | fields: { 711 | name: { 712 | // name field configuration here.. 713 | }, 714 | surname: { 715 | // surname field configuration here.. 716 | } 717 | } 718 | }; 719 | ``` 720 | 721 | ## Textbox component 722 | 723 | Implementation: `TextInput` 724 | 725 | **Tech note.** Values containing only white spaces are converted to `null`. 726 | 727 | ### Placeholder 728 | 729 | You can set the placeholder with the `placeholder` option: 730 | 731 | ```js 732 | var options = { 733 | fields: { 734 | name: { 735 | placeholder: 'Your placeholder here' 736 | } 737 | } 738 | }; 739 | ``` 740 | 741 | ### Label 742 | 743 | You can set the label with the `label` option: 744 | 745 | ```js 746 | var options = { 747 | fields: { 748 | name: { 749 | label: 'Insert your name' 750 | } 751 | } 752 | }; 753 | ``` 754 | 755 | ### Help message 756 | 757 | You can set a help message with the `help` option: 758 | 759 | ```js 760 | var options = { 761 | fields: { 762 | name: { 763 | help: 'Your help message here' 764 | } 765 | } 766 | }; 767 | ``` 768 | 769 | ![Help](docs/images/help.png) 770 | 771 | ### Error messages 772 | 773 | You can add a custom error message with the `error` option: 774 | 775 | ```js 776 | var options = { 777 | fields: { 778 | email: { 779 | // you can use strings or JSX 780 | error: 'Insert a valid email' 781 | } 782 | } 783 | }; 784 | ``` 785 | 786 | ![Help](docs/images/error.png) 787 | 788 | tcomb-form-native will display the error message when the field validation fails. 789 | 790 | `error` can also be a function with the following signature: 791 | 792 | ``` 793 | (value, path, context) => ?(string | ReactElement) 794 | ``` 795 | 796 | where 797 | 798 | - `value` is an object containing the current form value. 799 | - `path` is the path of the value being validated 800 | - `context` is the value of the `context` prop. Also it contains a reference to the component options. 801 | 802 | The value returned by the function will be used as error message. 803 | 804 | If you want to show the error message onload, add the `hasError` option: 805 | 806 | ```js 807 | var options = { 808 | hasError: true, 809 | error: A custom error message 810 | }; 811 | ``` 812 | 813 | Another way is to add a: 814 | 815 | ``` 816 | getValidationErrorMessage(value, path, context) 817 | ``` 818 | 819 | static function to a type, where: 820 | 821 | - `value` is the (parsed) current value of the component. 822 | - `path` is the path of the value being validated 823 | - `context` is the value of the `context` prop. Also it contains a reference to the component options. 824 | 825 | 826 | ```js 827 | var Age = t.refinement(t.Number, function (n) { return n >= 18; }); 828 | 829 | // if you define a getValidationErrorMessage function, it will be called on validation errors 830 | Age.getValidationErrorMessage = function (value, path, context) { 831 | return 'bad age, locale: ' + context.locale; 832 | }; 833 | 834 | var Schema = t.struct({ 835 | age: Age 836 | }); 837 | 838 | ... 839 | 840 | 845 | ``` 846 | 847 | You can even define `getValidationErrorMessage` on the supertype in order to be DRY: 848 | 849 | ```js 850 | t.Number.getValidationErrorMessage = function (value, path, context) { 851 | return 'bad number'; 852 | }; 853 | 854 | Age.getValidationErrorMessage = function (value, path, context) { 855 | return 'bad age, locale: ' + context.locale; 856 | }; 857 | ``` 858 | 859 | ### Other standard options 860 | 861 | The following standard options are available (see http://facebook.github.io/react-native/docs/textinput.html): 862 | 863 | - `autoCapitalize` 864 | - `autoCorrect` 865 | - `autoFocus` 866 | - `bufferDelay` 867 | - `clearButtonMode` 868 | - `editable` 869 | - `enablesReturnKeyAutomatically` 870 | - `keyboardType` 871 | - `maxLength` 872 | - `multiline` 873 | - `numberOfLines` 874 | - `onBlur` 875 | - `onEndEditing` 876 | - `onFocus` 877 | - `onSubmitEditing` 878 | - `onContentSizeChange` 879 | - `password` 880 | - `placeholderTextColor` 881 | - `returnKeyType` 882 | - `selectTextOnFocus` 883 | - `secureTextEntry` 884 | - `selectionState` 885 | - `textAlign` 886 | - `textAlignVertical` 887 | - `underlineColorAndroid` 888 | 889 | ## Checkbox component 890 | 891 | Implementation: `SwitchIOS` 892 | 893 | The following options are similar to the `Textbox` component's ones: 894 | 895 | - `label` 896 | - `help` 897 | - `error` 898 | 899 | ### Other standard options 900 | 901 | The following standard options are available (see http://facebook.github.io/react-native/docs/switchios.html): 902 | 903 | - `disabled` 904 | - `onTintColor` 905 | - `thumbTintColor` 906 | - `tintColor` 907 | 908 | ## Select component 909 | 910 | Implementation: `PickerIOS` 911 | 912 | The following options are similar to the `Textbox` component's ones: 913 | 914 | - `label` 915 | - `help` 916 | - `error` 917 | 918 | ### `nullOption` option 919 | 920 | You can customize the null option with the `nullOption` option: 921 | 922 | ```js 923 | var options = { 924 | fields: { 925 | gender: { 926 | nullOption: {value: '', text: 'Choose your gender'} 927 | } 928 | } 929 | }; 930 | ``` 931 | 932 | You can remove the null option setting the `nullOption` option to `false`. 933 | 934 | **Warning**: when you set `nullOption = false` you must also set the Form's `value` prop for the select field. 935 | 936 | **Tech note.** A value equal to `nullOption.value` (default `''`) is converted to `null`. 937 | 938 | ### Options order 939 | 940 | You can sort the options with the `order` option: 941 | 942 | ```js 943 | var options = { 944 | fields: { 945 | gender: { 946 | order: 'asc' // or 'desc' 947 | } 948 | } 949 | }; 950 | ``` 951 | 952 | ### Options isCollapsed 953 | 954 | You can determinate if Select is collapsed: 955 | 956 | ```js 957 | var options = { 958 | fields: { 959 | gender: { 960 | isCollapsed: false // default: true 961 | } 962 | } 963 | }; 964 | ``` 965 | 966 | If option not set, default is `true` 967 | 968 | ### Options onCollapseChange 969 | 970 | You can set a callback, triggered, when collapse change: 971 | 972 | ```js 973 | var options = { 974 | fields: { 975 | gender: { 976 | onCollapseChange: () => { console.log('collapse changed'); } 977 | } 978 | } 979 | }; 980 | ``` 981 | 982 | ## DatePicker component 983 | 984 | Implementation: `DatePickerIOS` 985 | 986 | ### Example 987 | 988 | ```js 989 | var Person = t.struct({ 990 | name: t.String, 991 | birthDate: t.Date 992 | }); 993 | ``` 994 | 995 | The following options are similar to the `Textbox` component's ones: 996 | 997 | - `label` 998 | - `help` 999 | - `error` 1000 | 1001 | ### Other standard options 1002 | 1003 | The following standard options are available (see http://facebook.github.io/react-native/docs/datepickerios.html): 1004 | 1005 | - `maximumDate`, 1006 | - `minimumDate`, 1007 | - `minuteInterval`, 1008 | - `mode`, 1009 | - `timeZoneOffsetInMinutes` 1010 | 1011 | ## Hidden Component 1012 | 1013 | For any component, you can set the field with the `hidden` option: 1014 | 1015 | ```js 1016 | var options = { 1017 | fields: { 1018 | name: { 1019 | hidden: true 1020 | } 1021 | } 1022 | }; 1023 | ``` 1024 | 1025 | This will completely skip the rendering of the component, while the default value will be available for validation purposes. 1026 | 1027 | # Unions 1028 | 1029 | **Code Example** 1030 | 1031 | ```js 1032 | const AccountType = t.enums.of([ 1033 | 'type 1', 1034 | 'type 2', 1035 | 'other' 1036 | ], 'AccountType') 1037 | 1038 | const KnownAccount = t.struct({ 1039 | type: AccountType 1040 | }, 'KnownAccount') 1041 | 1042 | // UnknownAccount extends KnownAccount so it owns also the type field 1043 | const UnknownAccount = KnownAccount.extend({ 1044 | label: t.String, 1045 | }, 'UnknownAccount') 1046 | 1047 | // the union 1048 | const Account = t.union([KnownAccount, UnknownAccount], 'Account') 1049 | 1050 | // the final form type 1051 | const Type = t.list(Account) 1052 | 1053 | const options = { 1054 | item: [ // one options object for each concrete type of the union 1055 | { 1056 | label: 'KnownAccount' 1057 | }, 1058 | { 1059 | label: 'UnknownAccount' 1060 | } 1061 | ] 1062 | } 1063 | ``` 1064 | 1065 | Generally `tcomb`'s unions require a `dispatch` implementation in order to select the suitable type constructor for a given value and this would be the key in this use case: 1066 | 1067 | ```js 1068 | // if account type is 'other' return the UnknownAccount type 1069 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount 1070 | ``` 1071 | 1072 | # Lists 1073 | 1074 | You can handle a list with the `t.list` combinator: 1075 | 1076 | ```js 1077 | const Person = t.struct({ 1078 | name: t.String, 1079 | tags: t.list(t.String) // a list of strings 1080 | }); 1081 | ``` 1082 | 1083 | ## Items configuration 1084 | 1085 | To configure all the items in a list, set the `item` option: 1086 | 1087 | ```js 1088 | const Person = t.struct({ 1089 | name: t.String, 1090 | tags: t.list(t.String) // a list of strings 1091 | }); 1092 | 1093 | const options = { 1094 | fields: { // <= Person options 1095 | tags: { 1096 | item: { // <= options applied to each item in the list 1097 | label: 'My tag' 1098 | } 1099 | } 1100 | } 1101 | }); 1102 | ``` 1103 | 1104 | ## Nested structures 1105 | 1106 | You can nest lists and structs at an arbitrary level: 1107 | 1108 | ```js 1109 | const Person = t.struct({ 1110 | name: t.String, 1111 | surname: t.String 1112 | }); 1113 | 1114 | const Persons = t.list(Person); 1115 | ``` 1116 | 1117 | If you want to provide options for your nested structures they must be nested 1118 | following the type structure. Here is an example: 1119 | 1120 | ```js 1121 | const Person = t.struct({ 1122 | name: t.Struct, 1123 | position: t.Struct({ 1124 | latitude: t.Number, 1125 | longitude: t.Number 1126 | }); 1127 | }); 1128 | 1129 | const options = { 1130 | fields: { // <= Person options 1131 | name: { 1132 | label: 'name label' 1133 | } 1134 | position: { 1135 | fields: { 1136 | // Note that latitude is not directly nested in position, 1137 | // but in the fields property 1138 | latitude: { 1139 | label: 'My position label' 1140 | } 1141 | } 1142 | } 1143 | } 1144 | }); 1145 | ``` 1146 | 1147 | When dealing with `t.list`, make sure to declare the `fields` property inside the `item` property, as such: 1148 | 1149 | ```js 1150 | const Documents = t.struct({ 1151 | type: t.Number, 1152 | value: t.String 1153 | }) 1154 | 1155 | const Person = t.struct({ 1156 | name: t.Struct, 1157 | documents: t.list(Documents) 1158 | }); 1159 | 1160 | const options = { 1161 | fields: { 1162 | name: { /*...*/ }, 1163 | documents: { 1164 | item: { 1165 | fields: { 1166 | type: { 1167 | // Documents t.struct 'type' options 1168 | }, 1169 | value: { 1170 | // Documents t.struct 'value' options 1171 | } 1172 | } 1173 | } 1174 | } 1175 | } 1176 | } 1177 | ``` 1178 | 1179 | ## Internationalization 1180 | 1181 | You can override the default language (english) with the `i18n` option: 1182 | 1183 | ```js 1184 | const options = { 1185 | i18n: { 1186 | optional: ' (optional)', 1187 | required: '', 1188 | add: 'Add', // add button 1189 | remove: '✘', // remove button 1190 | up: '↑', // move up button 1191 | down: '↓' // move down button 1192 | } 1193 | }; 1194 | ``` 1195 | 1196 | ## Buttons configuration 1197 | 1198 | You can prevent operations on lists with the following options: 1199 | 1200 | - `disableAdd`: (default `false`) prevents adding new items 1201 | - `disableRemove`: (default `false`) prevents removing existing items 1202 | - `disableOrder`: (default `false`) prevents sorting existing items 1203 | 1204 | ```js 1205 | const options = { 1206 | disableOrder: true 1207 | }; 1208 | ``` 1209 | 1210 | ## List with Dynamic Items (Different structs based on selected value) 1211 | 1212 | Lists of different types are not supported. This is because a `tcomb`'s list, by definition, contains only values of the same type. You can define a union though: 1213 | 1214 | ```js 1215 | const AccountType = t.enums.of([ 1216 | 'type 1', 1217 | 'type 2', 1218 | 'other' 1219 | ], 'AccountType') 1220 | 1221 | const KnownAccount = t.struct({ 1222 | type: AccountType 1223 | }, 'KnownAccount') 1224 | 1225 | // UnknownAccount extends KnownAccount so it owns also the type field 1226 | const UnknownAccount = KnownAccount.extend({ 1227 | label: t.String, 1228 | }, 'UnknownAccount') 1229 | 1230 | // the union 1231 | const Account = t.union([KnownAccount, UnknownAccount], 'Account') 1232 | 1233 | // the final form type 1234 | const Type = t.list(Account) 1235 | ``` 1236 | 1237 | Generally `tcomb`'s unions require a `dispatch` implementation in order to select the suitable type constructor for a given value and this would be the key in this use case: 1238 | 1239 | ```js 1240 | // if account type is 'other' return the UnknownAccount type 1241 | Account.dispatch = value => value && value.type === 'other' ? UnknownAccount : KnownAccount 1242 | ``` 1243 | 1244 | # Customizations 1245 | 1246 | ## Stylesheets 1247 | 1248 | See also [Stylesheet guide](docs/STYLESHEETS.md). 1249 | 1250 | tcomb-form-native comes with a default style. You can customize the look and feel by setting another stylesheet: 1251 | 1252 | ```js 1253 | var t = require('tcomb-form-native/lib'); 1254 | var i18n = require('tcomb-form-native/lib/i18n/en'); 1255 | var templates = require('tcomb-form-native/lib/templates/bootstrap'); 1256 | 1257 | // define a stylesheet (see lib/stylesheets/bootstrap for an example) 1258 | var stylesheet = {...}; 1259 | 1260 | // override globally the default stylesheet 1261 | t.form.Form.stylesheet = stylesheet; 1262 | // set defaults 1263 | t.form.Form.templates = templates; 1264 | t.form.Form.i18n = i18n; 1265 | ``` 1266 | 1267 | You can also override the stylesheet locally for selected fields: 1268 | 1269 | ```js 1270 | var Person = t.struct({ 1271 | name: t.String 1272 | }); 1273 | 1274 | var options = { 1275 | fields: { 1276 | name: { 1277 | stylesheet: myCustomStylesheet 1278 | } 1279 | } 1280 | }; 1281 | ``` 1282 | 1283 | Or per form: 1284 | 1285 | ```js 1286 | var Person = t.struct({ 1287 | name: t.String 1288 | }); 1289 | 1290 | var options = { 1291 | stylesheet: myCustomStylesheet 1292 | }; 1293 | ``` 1294 | 1295 | For a complete example see the default stylesheet https://github.com/gcanti/tcomb-form-native/blob/master/lib/stylesheets/bootstrap.js. 1296 | 1297 | ## Templates 1298 | 1299 | tcomb-form-native comes with a default layout, i.e. a bunch of templates, one for each component. 1300 | When changing the stylesheet is not enough, you can customize the layout by setting custom templates: 1301 | 1302 | ```js 1303 | var t = require('tcomb-form-native/lib'); 1304 | var i18n = require('tcomb-form-native/lib/i18n/en'); 1305 | var stylesheet = require('tcomb-form-native/lib/stylesheets/bootstrap'); 1306 | 1307 | // define the templates (see lib/templates/bootstrap for an example) 1308 | var templates = {...}; 1309 | 1310 | // override globally the default layout 1311 | t.form.Form.templates = templates; 1312 | // set defaults 1313 | t.form.Form.stylesheet = stylesheet; 1314 | t.form.Form.i18n = i18n; 1315 | ``` 1316 | 1317 | You can also override the template locally: 1318 | 1319 | ```js 1320 | var Person = t.struct({ 1321 | name: t.String 1322 | }); 1323 | 1324 | function myCustomTemplate(locals) { 1325 | 1326 | var containerStyle = {...}; 1327 | var labelStyle = {...}; 1328 | var textboxStyle = {...}; 1329 | 1330 | return ( 1331 | 1332 | {locals.label} 1333 | 1334 | 1335 | ); 1336 | } 1337 | 1338 | var options = { 1339 | fields: { 1340 | name: { 1341 | template: myCustomTemplate 1342 | } 1343 | } 1344 | }; 1345 | ``` 1346 | 1347 | A template is a function with the following signature: 1348 | 1349 | ``` 1350 | (locals: Object) => ReactElement 1351 | ``` 1352 | 1353 | where `locals` is an object contaning the "recipe" for rendering the input and it's built for you by tcomb-form-native. 1354 | Let's see an example: the `locals` object passed in the `checkbox` template: 1355 | 1356 | ```js 1357 | type Message = string | ReactElement 1358 | 1359 | { 1360 | stylesheet: Object, // the styles to be applied 1361 | hasError: boolean, // true if there is a validation error 1362 | error: ?Message, // the optional error message to be displayed 1363 | label: Message, // the label to be displayed 1364 | help: ?Message, // the optional help message to be displayed 1365 | value: boolean, // the current value of the checkbox 1366 | onChange: Function, // the event handler to be called when the value changes 1367 | config: Object, // an optional object to pass configuration options to the new template 1368 | 1369 | ...other input options here... 1370 | 1371 | } 1372 | ``` 1373 | 1374 | For a complete example see the default template https://github.com/gcanti/tcomb-form-native/blob/master/lib/templates/bootstrap. 1375 | 1376 | ## i18n 1377 | 1378 | tcomb-form-native comes with a default internationalization (English). You can change it by setting another i18n object: 1379 | 1380 | ```js 1381 | var t = require('tcomb-form-native/lib'); 1382 | var templates = require('tcomb-form-native/lib/templates/bootstrap'); 1383 | 1384 | // define an object containing your translations (see tcomb-form-native/lib/i18n/en for an example) 1385 | var i18n = {...}; 1386 | 1387 | // override globally the default i18n 1388 | t.form.Form.i18n = i18n; 1389 | // set defaults 1390 | t.form.Form.templates = templates; 1391 | t.form.Form.stylesheet = stylesheet; 1392 | ``` 1393 | 1394 | ## Transformers 1395 | 1396 | Say you want a search textbox which accepts a list of keywords separated by spaces: 1397 | 1398 | ```js 1399 | var Search = t.struct({ 1400 | search: t.list(t.String) 1401 | }); 1402 | ``` 1403 | 1404 | tcomb-form by default will render the `search` field as a list. In order to render a textbox you have to override the default behaviour with the factory option: 1405 | 1406 | ```js 1407 | var options = { 1408 | fields: { 1409 | search: { 1410 | factory: t.form.Textbox 1411 | } 1412 | } 1413 | }; 1414 | ``` 1415 | 1416 | There is a problem though: a textbox handle only strings so we need a way to transform a list in a string and a string in a list: a `Transformer` deals with serialization / deserialization of data and has the following interface: 1417 | 1418 | ```js 1419 | var Transformer = t.struct({ 1420 | format: t.Function, // from value to string, it must be idempotent 1421 | parse: t.Function // from string to value 1422 | }); 1423 | ``` 1424 | 1425 | A basic transformer implementation for the search textbox: 1426 | 1427 | ```js 1428 | var listTransformer = { 1429 | format: function (value) { 1430 | return Array.isArray(value) ? value.join(' ') : value; 1431 | }, 1432 | parse: function (str) { 1433 | return str ? str.split(' ') : []; 1434 | } 1435 | }; 1436 | ``` 1437 | 1438 | Now you can handle lists using the transformer option: 1439 | 1440 | ```js 1441 | // example of initial value 1442 | var value = { 1443 | search: ['climbing', 'yosemite'] 1444 | }; 1445 | 1446 | var options = { 1447 | fields: { 1448 | search: { 1449 | factory: t.form.Textbox, // tell tcomb-react-native to use the same component for textboxes 1450 | transformer: listTransformer, 1451 | help: 'Keywords are separated by spaces' 1452 | } 1453 | } 1454 | }; 1455 | ``` 1456 | 1457 | ## Custom factories 1458 | 1459 | You can pack together style, template (and transformers) in a custom component and then you can use it with the `factory` option: 1460 | 1461 | ```js 1462 | var Component = t.form.Component; 1463 | 1464 | // extend the base Component 1465 | class MyComponent extends Component { 1466 | 1467 | // this is the only required method to implement 1468 | getTemplate() { 1469 | // define here your custom template 1470 | return function (locals) { 1471 | 1472 | //return ... jsx ... 1473 | 1474 | }; 1475 | } 1476 | 1477 | // you can optionally override the default getLocals method 1478 | // it will provide the locals param to your template 1479 | getLocals() { 1480 | 1481 | // in locals you'll find the default locals: 1482 | // - path 1483 | // - error 1484 | // - hasError 1485 | // - label 1486 | // - onChange 1487 | // - stylesheet 1488 | var locals = super.getLocals(); 1489 | 1490 | // add here your custom locals 1491 | 1492 | return locals; 1493 | } 1494 | 1495 | 1496 | } 1497 | 1498 | // as example of transformer: this is the default transformer for textboxes 1499 | MyComponent.transformer = { 1500 | format: value => Nil.is(value) ? null : value, 1501 | parse: value => (t.String.is(value) && value.trim() === '') || Nil.is(value) ? null : value 1502 | }; 1503 | 1504 | var Person = t.struct({ 1505 | name: t.String 1506 | }); 1507 | 1508 | var options = { 1509 | fields: { 1510 | name: { 1511 | factory: MyComponent 1512 | } 1513 | } 1514 | }; 1515 | ``` 1516 | 1517 | # Tests 1518 | 1519 | ``` 1520 | npm test 1521 | ``` 1522 | **Note:** If you are using Jest, you will encounter an error which can 1523 | be fixed w/ a small change to the ```package.json```. 1524 | 1525 | The error will look similiar to the following: 1526 | ``` 1527 | Error: Cannot find module './datepicker' from 'index.js' at 1528 | Resolver.resolveModule 1529 | ``` 1530 | 1531 | A completely working example ```jest``` setup is shown below w/ the 1532 | [http://facebook.github.io/jest/docs/api.html#modulefileextensions-array-string](http://facebook.github.io/jest/docs/api.html#modulefileextensions-array-string) 1533 | fix added: 1534 | 1535 | ``` 1536 | "jest": { 1537 | "setupEnvScriptFile": "./node_modules/react-native/jestSupport/env.js", 1538 | "haste": { 1539 | "defaultPlatform": "ios", 1540 | "platforms": [ 1541 | "ios", 1542 | "android" 1543 | ], 1544 | "providesModuleNodeModules": [ 1545 | "react-native" 1546 | ] 1547 | }, 1548 | "testPathIgnorePatterns": [ 1549 | "/node_modules/" 1550 | ], 1551 | "testFileExtensions": [ 1552 | "es6", 1553 | "js" 1554 | ], 1555 | "moduleFileExtensions": [ 1556 | "js", 1557 | "json", 1558 | "es6", 1559 | "ios.js" <<<<<<<<<<<< this needs to be defined! 1560 | ], 1561 | "unmockedModulePathPatterns": [ 1562 | "react", 1563 | "react-addons-test-utils", 1564 | "react-native-router-flux", 1565 | "promise", 1566 | "source-map", 1567 | "key-mirror", 1568 | "immutable", 1569 | "fetch", 1570 | "redux", 1571 | "redux-thunk", 1572 | "fbjs" 1573 | ], 1574 | "collectCoverage": false, 1575 | "verbose": true 1576 | }, 1577 | ``` 1578 | 1579 | # License 1580 | 1581 | [MIT](LICENSE) 1582 | --------------------------------------------------------------------------------